18 Commits

Author SHA1 Message Date
4d180871ca Readme updated 2021-09-03 01:25:56 +02:00
b87bff54c4 Little code organization tweaks 2021-09-03 00:21:15 +02:00
be8cef35d3 Resources file management, french translation and a bit of design 2021-09-02 19:47:55 +02:00
c568a957f7 MPD handles disconnection and reconnection 2021-09-02 00:14:28 +02:00
2c8696155a Handle no title for song and stop state 2021-09-01 10:21:52 +02:00
48b5bc5d28 Pass of code standardisation 2021-09-01 02:35:37 +02:00
984d2056de Button border thickness declared in XAML and not in code 2021-08-31 02:25:42 +02:00
86a652c170 Improvement on seek time slider 2021-08-31 01:53:52 +02:00
0aa80ed8b5 Option to show Snapcast window 2021-08-31 01:42:00 +02:00
72d2c5993d UI is now updated through MPD events and not every second + misc fixes 2021-08-31 01:30:00 +02:00
1b44c64ec5 Fix playback but is not resistant to disconnection 2021-08-29 20:50:47 +02:00
80d6395c8e First implementation of MpcNET
Exceptions thrown for playback to investigate, but get data is okay
2021-08-29 00:32:41 +02:00
765cb4f4ea New icons and publish profile 2021-08-27 23:28:27 +02:00
dda239ec34 Usable sliders for time and volume 2021-08-27 19:49:57 +02:00
8896c13442 Misc. fixes, added volume offset option 2021-08-27 18:45:53 +02:00
89ddb81840 Snapcast text notify working 2021-08-20 18:26:32 +02:00
60ca1b6e9a Update visual style for player 2021-08-20 16:50:50 +02:00
553ca54315 Change buttons 2021-08-19 23:03:50 +02:00
35 changed files with 2122 additions and 6117 deletions

256
App.xaml
View File

@ -8,7 +8,263 @@
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Views/Systray.xaml" />
<ResourceDictionary Source="Resources/Resources.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style x:Key="RepeatButtonTransparent" TargetType="{x:Type RepeatButton}">
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Focusable" Value="false"/>
<Setter Property="IsTabStop" Value="false"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RepeatButton}">
<Rectangle Fill="{TemplateBinding Background}" Height="{TemplateBinding Height}" Width="{TemplateBinding Width}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<SolidColorBrush x:Key="SliderThumb.Static.Background" Color="#FFF0F0F0"/>
<SolidColorBrush x:Key="SliderThumb.Static.Border" Color="#FFACACAC"/>
<SolidColorBrush x:Key="SliderThumb.Static.Foreground" Color="#FFE5E5E5"/>
<SolidColorBrush x:Key="SliderThumb.MouseOver.Background" Color="#FFDCECFC"/>
<SolidColorBrush x:Key="SliderThumb.MouseOver.Border" Color="#FF7Eb4EA"/>
<SolidColorBrush x:Key="SliderThumb.Pressed.Background" Color="#FFDAECFC"/>
<SolidColorBrush x:Key="SliderThumb.Pressed.Border" Color="#FF569DE5"/>
<SolidColorBrush x:Key="SliderThumb.Disabled.Background" Color="#FFF0F0F0"/>
<SolidColorBrush x:Key="SliderThumb.Disabled.Border" Color="#FFD9D9D9"/>
<SolidColorBrush x:Key="SliderThumb.Track.Background" Color="#FFE7EAEA"/>
<SolidColorBrush x:Key="SliderThumb.Track.Border" Color="#FFD6D6D6"/>
<ControlTemplate x:Key="SliderThumbHorizontalDefault" TargetType="{x:Type Thumb}">
<Grid HorizontalAlignment="Center" UseLayoutRounding="True" VerticalAlignment="Center">
<!--<Path x:Name="grip" Data="M 0,0 C0,0 11,0 11,0 11,0 11,18 11,18 11,18 0,18 0,18 0,18 0,0 0,0 z" Fill="{StaticResource SliderThumb.Static.Background}" SnapsToDevicePixels="True" StrokeThickness="1" Stretch="Fill" Stroke="{StaticResource SliderThumb.Static.Border}" UseLayoutRounding="True" VerticalAlignment="Center"/>-->
<Ellipse x:Name="grip" Width="15" Height="15" Fill="{DynamicResource SliderThumb.Disabled.Background}" Stretch="Fill" SnapsToDevicePixels="True" Stroke="{StaticResource SliderThumb.Static.Border}" StrokeThickness="1" UseLayoutRounding="True" VerticalAlignment="Center"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Border}"/>
</Trigger>
<Trigger Property="IsDragging" Value="true">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Border}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Border}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="SliderThumbHorizontalTop" TargetType="{x:Type Thumb}">
<Grid HorizontalAlignment="Center" UseLayoutRounding="True" VerticalAlignment="Center">
<Path x:Name="grip" Data="M 0,6 C0,6 5.5,0 5.5,0 5.5,0 11,6 11,6 11,6 11,18 11,18 11,18 0,18 0,18 0,18 0,6 0,6 z" Fill="{StaticResource SliderThumb.Static.Background}" SnapsToDevicePixels="True" StrokeThickness="1" Stretch="Fill" Stroke="{StaticResource SliderThumb.Static.Border}" UseLayoutRounding="True" VerticalAlignment="Center"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Border}"/>
</Trigger>
<Trigger Property="IsDragging" Value="true">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Border}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Border}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="SliderThumbHorizontalBottom" TargetType="{x:Type Thumb}">
<Grid HorizontalAlignment="Center" UseLayoutRounding="True" VerticalAlignment="Center">
<Path x:Name="grip" Data="M 0,12 C0,12 5.5,18 5.5,18 5.5,18 11,12 11,12 11,12 11,0 11,0 11,0 0,0 0,0 0,0 0,12 0,12 z" Fill="{StaticResource SliderThumb.Static.Background}" SnapsToDevicePixels="True" StrokeThickness="1" Stretch="Fill" Stroke="{StaticResource SliderThumb.Static.Border}" UseLayoutRounding="True" VerticalAlignment="Center"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Border}"/>
</Trigger>
<Trigger Property="IsDragging" Value="true">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Border}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Border}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="SliderHorizontal" TargetType="{x:Type Slider}">
<Border x:Name="border" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" SnapsToDevicePixels="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" MinHeight="{TemplateBinding MinHeight}"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TickBar x:Name="TopTick" Fill="{TemplateBinding Foreground}" Height="4" Margin="0,0,0,2" Placement="Top" Grid.Row="0" Visibility="Collapsed"/>
<TickBar x:Name="BottomTick" Fill="{TemplateBinding Foreground}" Height="4" Margin="0,2,0,0" Placement="Bottom" Grid.Row="2" Visibility="Collapsed"/>
<Border x:Name="TrackBackground" Background="{StaticResource SliderThumb.Track.Background}" BorderThickness="1" BorderBrush="{StaticResource SliderThumb.Track.Border}" Height="4.0" Margin="5,0" Grid.Row="1" VerticalAlignment="center">
<Canvas Margin="-6,-1">
<Rectangle x:Name="PART_SelectionRange" Fill="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" Height="4.0" Visibility="Hidden"/>
</Canvas>
</Border>
<Track x:Name="PART_Track" Grid.Row="1">
<Track.DecreaseRepeatButton>
<RepeatButton Command="{x:Static Slider.DecreaseLarge}" Style="{StaticResource RepeatButtonTransparent}"/>
</Track.DecreaseRepeatButton>
<Track.IncreaseRepeatButton>
<RepeatButton Command="{x:Static Slider.IncreaseLarge}" Style="{StaticResource RepeatButtonTransparent}"/>
</Track.IncreaseRepeatButton>
<Track.Thumb>
<Thumb x:Name="Thumb" Focusable="False" Height="20" OverridesDefaultStyle="True" Template="{StaticResource SliderThumbHorizontalDefault}" VerticalAlignment="Center" Width="20"/>
</Track.Thumb>
</Track>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="TickPlacement" Value="TopLeft">
<Setter Property="Visibility" TargetName="TopTick" Value="Visible"/>
<Setter Property="Template" TargetName="Thumb" Value="{StaticResource SliderThumbHorizontalTop}"/>
<Setter Property="Margin" TargetName="TrackBackground" Value="5,2,5,0"/>
</Trigger>
<Trigger Property="TickPlacement" Value="BottomRight">
<Setter Property="Visibility" TargetName="BottomTick" Value="Visible"/>
<Setter Property="Template" TargetName="Thumb" Value="{StaticResource SliderThumbHorizontalBottom}"/>
<Setter Property="Margin" TargetName="TrackBackground" Value="5,0,5,2"/>
</Trigger>
<Trigger Property="TickPlacement" Value="Both">
<Setter Property="Visibility" TargetName="TopTick" Value="Visible"/>
<Setter Property="Visibility" TargetName="BottomTick" Value="Visible"/>
</Trigger>
<Trigger Property="IsSelectionRangeEnabled" Value="true">
<Setter Property="Visibility" TargetName="PART_SelectionRange" Value="Visible"/>
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="true">
<Setter Property="Foreground" TargetName="Thumb" Value="Blue"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="SliderThumbVerticalDefault" TargetType="{x:Type Thumb}">
<Grid HorizontalAlignment="Center" UseLayoutRounding="True" VerticalAlignment="Center">
<Path x:Name="grip" Data="M0.5,0.5 L18.5,0.5 18.5,11.5 0.5,11.5z" Fill="{StaticResource SliderThumb.Static.Background}" Stretch="Fill" Stroke="{StaticResource SliderThumb.Static.Border}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Border}"/>
</Trigger>
<Trigger Property="IsDragging" Value="true">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Border}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Border}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="SliderThumbVerticalLeft" TargetType="{x:Type Thumb}">
<Grid HorizontalAlignment="Center" UseLayoutRounding="True" VerticalAlignment="Center">
<Path x:Name="grip" Data="M 6,11 C6,11 0,5.5 0,5.5 0,5.5 6,0 6,0 6,0 18,0 18,0 18,0 18,11 18,11 18,11 6,11 6,11 z" Fill="{StaticResource SliderThumb.Static.Background}" Stretch="Fill" Stroke="{StaticResource SliderThumb.Static.Border}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Border}"/>
</Trigger>
<Trigger Property="IsDragging" Value="true">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Border}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Border}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="SliderThumbVerticalRight" TargetType="{x:Type Thumb}">
<Grid HorizontalAlignment="Center" UseLayoutRounding="True" VerticalAlignment="Center">
<Path x:Name="grip" Data="M 12,11 C12,11 18,5.5 18,5.5 18,5.5 12,0 12,0 12,0 0,0 0,0 0,0 0,11 0,11 0,11 12,11 12,11 z" Fill="{StaticResource SliderThumb.Static.Background}" Stretch="Fill" Stroke="{StaticResource SliderThumb.Static.Border}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Border}"/>
</Trigger>
<Trigger Property="IsDragging" Value="true">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Border}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Background}"/>
<Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Border}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="SliderVertical" TargetType="{x:Type Slider}">
<Border x:Name="border" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" SnapsToDevicePixels="True">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition MinWidth="{TemplateBinding MinWidth}" Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TickBar x:Name="TopTick" Grid.Column="0" Fill="{TemplateBinding Foreground}" Margin="0,0,2,0" Placement="Left" Visibility="Collapsed" Width="4"/>
<TickBar x:Name="BottomTick" Grid.Column="2" Fill="{TemplateBinding Foreground}" Margin="2,0,0,0" Placement="Right" Visibility="Collapsed" Width="4"/>
<Border x:Name="TrackBackground" Background="{StaticResource SliderThumb.Track.Background}" BorderThickness="1" BorderBrush="{StaticResource SliderThumb.Track.Border}" Grid.Column="1" HorizontalAlignment="center" Margin="0,5" Width="4.0">
<Canvas Margin="-1,-6">
<Rectangle x:Name="PART_SelectionRange" Fill="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" Visibility="Hidden" Width="4.0"/>
</Canvas>
</Border>
<Track x:Name="PART_Track" Grid.Column="1">
<Track.DecreaseRepeatButton>
<RepeatButton Command="{x:Static Slider.DecreaseLarge}" Style="{StaticResource RepeatButtonTransparent}"/>
</Track.DecreaseRepeatButton>
<Track.IncreaseRepeatButton>
<RepeatButton Command="{x:Static Slider.IncreaseLarge}" Style="{StaticResource RepeatButtonTransparent}"/>
</Track.IncreaseRepeatButton>
<Track.Thumb>
<Thumb x:Name="Thumb" Focusable="False" Height="11" OverridesDefaultStyle="True" Template="{StaticResource SliderThumbVerticalDefault}" VerticalAlignment="Top" Width="18"/>
</Track.Thumb>
</Track>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="TickPlacement" Value="TopLeft">
<Setter Property="Visibility" TargetName="TopTick" Value="Visible"/>
<Setter Property="Template" TargetName="Thumb" Value="{StaticResource SliderThumbVerticalLeft}"/>
<Setter Property="Margin" TargetName="TrackBackground" Value="2,5,0,5"/>
</Trigger>
<Trigger Property="TickPlacement" Value="BottomRight">
<Setter Property="Visibility" TargetName="BottomTick" Value="Visible"/>
<Setter Property="Template" TargetName="Thumb" Value="{StaticResource SliderThumbVerticalRight}"/>
<Setter Property="Margin" TargetName="TrackBackground" Value="0,5,2,5"/>
</Trigger>
<Trigger Property="TickPlacement" Value="Both">
<Setter Property="Visibility" TargetName="TopTick" Value="Visible"/>
<Setter Property="Visibility" TargetName="BottomTick" Value="Visible"/>
</Trigger>
<Trigger Property="IsSelectionRangeEnabled" Value="true">
<Setter Property="Visibility" TargetName="PART_SelectionRange" Value="Visible"/>
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="true">
<Setter Property="Foreground" TargetName="Thumb" Value="Blue"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<Style x:Key="SliderTime" TargetType="{x:Type Slider}">
<Setter Property="Stylus.IsPressAndHoldEnabled" Value="false"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource SliderThumb.Static.Foreground}"/>
<Setter Property="Template" Value="{StaticResource SliderHorizontal}"/>
<Style.Triggers>
<Trigger Property="Orientation" Value="Vertical">
<Setter Property="Template" Value="{StaticResource SliderVertical}"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</Application.Resources>

View File

@ -1,51 +1,43 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Windows;
using Hardcodet.Wpf.TaskbarNotification;
// todo:
//
// * finish correct refresh of mpd
// * show mpd version
// * change volume offset
// * fix window resize
// * show covers
namespace unison
{
public partial class App : Application
{
private TaskbarIcon Systray;
private HotkeyHandler Hotkeys;
private SnapcastHandler Snapcast;
private MPDHandler MPD;
private TaskbarIcon _systray;
private HotkeyHandler _hotkeys;
private SnapcastHandler _snapcast;
private MPDHandler _mpd;
protected override void OnStartup(StartupEventArgs e)
{
//debug language
//unison.Resources.Resources.Culture = CultureInfo.GetCultureInfo("fr-FR");
base.OnStartup(e);
Snapcast = new SnapcastHandler();
Current.Properties["snapcast"] = Snapcast;
_mpd = new MPDHandler();
Current.Properties["mpd"] = _mpd;
MPD = new MPDHandler();
Current.Properties["mpd"] = MPD;
_hotkeys = new HotkeyHandler();
Current.Properties["hotkeys"] = _hotkeys;
Hotkeys = new HotkeyHandler();
Current.Properties["hotkeys"] = Hotkeys;
_snapcast = new SnapcastHandler();
Current.Properties["snapcast"] = _snapcast;
Current.MainWindow = new MainWindow();
Systray = (TaskbarIcon)FindResource("SystrayTaskbar");
Current.Properties["systray"] = Systray;
MPD.Start();
_systray = (TaskbarIcon)FindResource("SystrayTaskbar");
Current.Properties["systray"] = _systray;
}
protected override void OnExit(ExitEventArgs e)
{
Systray.Dispose();
Snapcast.Stop();
Hotkeys.RemoveHotKeys();
_systray.Dispose();
_snapcast.LaunchOrExit(true);
_hotkeys.RemoveHotKeys();
base.OnExit(e);
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
@ -31,14 +30,15 @@ namespace unison
private const uint VK_VOLUME_DOWN = 0xAE;
private const uint VK_ENTER = 0x0D;
private MainWindow _appWindow;
private readonly MPDHandler _mpd;
private IntPtr _windowHandle;
private HwndSource _source;
private readonly MPDHandler mpd;
public HotkeyHandler()
{
mpd = (MPDHandler)Application.Current.Properties["mpd"];
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
}
public void Activate(Window win)
@ -65,37 +65,46 @@ namespace unison
if (msg == WM_HOTKEY && wParam.ToInt32() == HOTKEY_ID)
{
uint vkey = ((uint)lParam >> 16) & 0xFFFF;
MainWindow AppWindow = (MainWindow)Application.Current.MainWindow;
switch (vkey)
{
case VK_MEDIA_NEXT_TRACK:
mpd.Next();
_mpd.Next();
break;
case VK_MEDIA_PREV_TRACK:
mpd.Prev();
_mpd.Prev();
break;
case VK_VOLUME_DOWN:
mpd._currentVolume -= 5;
mpd.SetVolume(mpd._currentVolume);
_mpd.VolumeDown();
break;
case VK_VOLUME_UP:
mpd._currentVolume += 5;
mpd.SetVolume(mpd._currentVolume);
_mpd.VolumeUp();
break;
case VK_MEDIA_PLAY_PAUSE:
mpd.PlayPause();
_mpd.PlayPause();
break;
case VK_ENTER:
if (AppWindow.WindowState == WindowState.Minimized)
if (_appWindow == null)
_appWindow = (MainWindow)Application.Current.MainWindow;
if (_appWindow.WindowState == WindowState.Minimized)
{
AppWindow.Show();
AppWindow.Activate();
AppWindow.WindowState = WindowState.Normal;
_appWindow.Show();
_appWindow.Activate();
_appWindow.WindowState = WindowState.Normal;
}
else
{
AppWindow.Hide();
AppWindow.WindowState = WindowState.Minimized;
if (_appWindow.IsActive)
{
_appWindow.Hide();
_appWindow.WindowState = WindowState.Minimized;
}
else // not minimized but not in front
{
_appWindow.Show();
_appWindow.Activate();
_appWindow.WindowState = WindowState.Normal;
}
}
break;
}

View File

@ -1,295 +1,443 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using MPDCtrl.Models;
using MpcNET;
using MpcNET.Commands.Database;
using MpcNET.Commands.Playback;
using MpcNET.Commands.Status;
using MpcNET.Message;
using MpcNET.Types;
namespace unison
{
public class MPDHandler
{
private readonly MPC _mpd = new();
public bool _connected;
public int _currentVolume;
public bool _currentRandom;
public bool _currentRepeat;
public bool _currentSingle;
public bool _currentConsume;
public double _currentElapsed;
private Status _currentStatus = null;
private SongInfoEx _currentSong = null;
private AlbumImage _currentAlbumCover = null;
public double _elapsed;
private double _time;
private bool _connected;
public string _version;
private int _currentVolume;
private bool _currentRandom;
private bool _currentRepeat;
private bool _currentSingle;
private bool _currentConsume;
private double _currentTime;
private double _totalTime;
private MpdStatus _currentStatus;
private IMpdFile _currentSong;
private BitmapFrame _cover;
private readonly System.Timers.Timer _elapsedTimer;
private async void ElapsedTimer(object sender, System.Timers.ElapsedEventArgs e)
{
if ((_elapsed < _time) && (_mpd.MpdStatus.MpdState == Status.MpdPlayState.Play))
{
_elapsed += 0.5;
await Task.Delay(5);
}
else
{
_elapsedTimer.Stop();
}
}
private DispatcherTimer _retryTimer;
bool IsBusy = false;
bool _isUpdatingStatus = false;
bool _isUpdatingSong = false;
private event EventHandler ConnectionChanged;
private event EventHandler StatusChanged;
private event EventHandler SongChanged;
private event EventHandler CoverChanged;
private MpcConnection _connection;
private MpcConnection _commandConnection;
private IPEndPoint _mpdEndpoint;
private CancellationTokenSource cancelToken;
public MPDHandler()
{
_mpd.IsBusy += new MPC.IsBusyEvent(OnMpcIsBusy);
cancelToken = new CancellationTokenSource();
_mpd.MpdIdleConnected += new MPC.IsMpdIdleConnectedEvent(OnMpdIdleConnected);
Initialize(null, null);
_mpd.ConnectionStatusChanged += new MPC.ConnectionStatusChangedEvent(OnConnectionStatusChanged);
_mpd.ConnectionError += new MPC.ConnectionErrorEvent(OnConnectionError);
_mpd.MpdPlayerStatusChanged += new MPC.MpdPlayerStatusChangedEvent(OnMpdPlayerStatusChanged);
_mpd.MpdCurrentQueueChanged += new MPC.MpdCurrentQueueChangedEvent(OnMpdCurrentQueueChanged);
_mpd.MpdAlbumArtChanged += new MPC.MpdAlbumArtChangedEvent(OnAlbumArtChanged);
_retryTimer = new DispatcherTimer();
_retryTimer.Interval = TimeSpan.FromSeconds(5);
_retryTimer.Tick += Initialize;
_elapsedTimer = new System.Timers.Timer(500);
_elapsedTimer.Elapsed += new System.Timers.ElapsedEventHandler(ElapsedTimer);
DispatcherTimer timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(2);
timer.Tick += QueryStatus;
timer.Start();
ConnectionChanged += OnConnectionChanged;
StatusChanged += OnStatusChanged;
SongChanged += OnSongChanged;
CoverChanged += OnCoverChanged;
}
private void OnMpcIsBusy(MPC sender, bool on)
private void ElapsedTimer(object sender, System.Timers.ElapsedEventArgs e)
{
IsBusy = on;
}
public void Start()
{
Task.Run(() => _mpd.MpdIdleConnect(Properties.Settings.Default.mpd_host, Properties.Settings.Default.mpd_port));
}
private void OnMpdIdleConnected(MPC sender)
{
Trace.WriteLine($"Connection to mpd {_mpd.MpdVerText}...");
LoadInitialData();
}
private void OnConnectionStatusChanged(MPC sender, MPC.ConnectionStatus status)
{
Trace.WriteLine("Connection changed...");
}
private void OnConnectionError(MPC sender, string msg)
{
Trace.WriteLine("Connection ERROR!");
LoadInitialData();
}
private void OnMpdPlayerStatusChanged(MPC sender)
{
Trace.WriteLine("Status changed...");
UpdateStatus();
}
private void OnMpdCurrentQueueChanged(MPC sender)
{
Trace.WriteLine("Queue changed...");
UpdateStatus();
}
private void OnAlbumArtChanged(MPC sender)
{
// AlbumArt
if (Application.Current == null) { return; }
Application.Current.Dispatcher.Invoke(() =>
{
if ((!_mpd.AlbumCover.IsDownloading) && _mpd.AlbumCover.IsSuccess)
{
if ((_mpd.MpdCurrentSong != null) && (_mpd.AlbumCover.AlbumImageSource != null))
{
if (!String.IsNullOrEmpty(_mpd.MpdCurrentSong.File))
{
if (_mpd.MpdCurrentSong.File == _mpd.AlbumCover.SongFilePath)
{
Trace.WriteLine("found cover");
_currentAlbumCover = _mpd.AlbumCover;
}
}
}
}
});
}
public async void LoadInitialData()
{
//todo : test if the password works
IsBusy = true;
await Task.Delay(5);
CommandResult result = await _mpd.MpdIdleSendPassword(Properties.Settings.Default.mpd_password);
if (result.IsSuccess)
{
_connected = await _mpd.MpdCommandConnectionStart(Properties.Settings.Default.mpd_host, Properties.Settings.Default.mpd_port, Properties.Settings.Default.mpd_password);
if (_connected)
{
await _mpd.MpdSendUpdate();
result = await _mpd.MpdIdleQueryStatus();
await Task.Delay(5);
if (result.IsSuccess)
{
_currentVolume = _mpd.MpdStatus.MpdVolume;
_currentRandom = _mpd.MpdStatus.MpdRandom;
_currentRepeat = _mpd.MpdStatus.MpdRepeat;
_currentSingle = _mpd.MpdStatus.MpdSingle;
_currentConsume = _mpd.MpdStatus.MpdConsume;
_currentElapsed = _mpd.MpdStatus.MpdSongElapsed;
}
await Task.Delay(5);
CommandResult song = await _mpd.MpdIdleQueryCurrentSong();
await Task.Delay(5);
if (song != null)
_currentSong = _mpd.MpdCurrentSong;
await Task.Delay(5);
_mpd.MpdIdleStart();
await Task.Delay(5);
UpdateStatus();
}
}
}
public async void QueryStatus(object sender, EventArgs e)
{
if (IsBusy)
return;
Trace.WriteLine("Querying status...");
CommandResult result = await _mpd.MpdQueryStatus();
await Task.Delay(5);
if (result.IsSuccess)
{
result = await _mpd.MpdQueryCurrentSong();
await Task.Delay(5);
if (result.IsSuccess)
{
UpdateStatus();
}
}
}
public async void UpdateStatus()
{
if (!_connected)
return;
await Task.Delay(50);
_currentStatus = _mpd.MpdStatus;
_currentRandom = _mpd.MpdStatus.MpdRandom;
_currentRepeat = _mpd.MpdStatus.MpdRepeat;
_currentConsume = _mpd.MpdStatus.MpdConsume;
_currentSingle = _mpd.MpdStatus.MpdSingle;
_currentVolume = _mpd.MpdStatus.MpdVolume;
_currentElapsed = _mpd.MpdStatus.MpdSongElapsed;
_currentSong = _mpd.MpdCurrentSong;
_time = _mpd.MpdStatus.MpdSongTime;
_elapsed = _mpd.MpdStatus.MpdSongElapsed;
if (_mpd.MpdStatus.MpdState == Status.MpdPlayState.Play)
{
if (!_elapsedTimer.Enabled)
_elapsedTimer.Start();
}
if ((_currentTime < _totalTime || _totalTime == -1) && (_currentStatus.State == MpdState.Play))
_currentTime += 0.5;
else
{
_elapsedTimer.Stop();
}
await _mpd.MpdQueryAlbumArt(_currentSong.File, false);
void OnConnectionChanged(object sender, EventArgs e)
{
if (!_connected)
_retryTimer.Start();
else
_retryTimer.Stop();
Application.Current.Dispatcher.Invoke(() =>
{
MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
MainWin.OnConnectionChanged(sender, e);
SnapcastHandler Snapcast = (SnapcastHandler)Application.Current.Properties["snapcast"];
Snapcast.OnConnectionChanged(sender, e);
});
}
public SongInfoEx GetCurrentSong() => _currentSong;
public Status GetStatus() => _currentStatus;
public AlbumImage GetCover() => _currentAlbumCover;
public async void Prev()
static void OnStatusChanged(object sender, EventArgs e)
{
if (!IsBusy)
await _mpd.MpdPlaybackPrev(_currentVolume);
Application.Current.Dispatcher.Invoke(() =>
{
MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
MainWin.OnStatusChanged(sender, e);
});
}
public async void Next()
static void OnSongChanged(object sender, EventArgs e)
{
if (!IsBusy)
await _mpd.MpdPlaybackNext(_currentVolume);
Application.Current.Dispatcher.Invoke(() =>
{
MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
MainWin.OnSongChanged(sender, e);
});
}
public async void PlayPause()
static void OnCoverChanged(object sender, EventArgs e)
{
if (IsBusy)
Application.Current.Dispatcher.Invoke(() =>
{
MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
MainWin.OnCoverChanged(sender, e);
});
}
public void SendCommand<T>(IMpcCommand<T> command)
{
Task.Run(async () =>
{
await SafelySendCommandAsync(command);
});
}
public async Task<T> SafelySendCommandAsync<T>(IMpcCommand<T> command)
{
if (_commandConnection == null)
{
Trace.WriteLine("[SafelySendCommandAsync] no command connection");
return default(T);
}
try
{
IMpdMessage<T> response = await _commandConnection.SendAsync(command);
if (!response.IsResponseValid)
{
var mpdError = response.Response?.Result?.MpdError;
if (mpdError != null && mpdError != "")
throw new Exception(mpdError);
else
throw new Exception($"Invalid server response: {response}.");
}
return response.Response.Content;
}
catch (Exception e)
{
Trace.WriteLine($"Sending {command.GetType().Name} failed: {e.Message}");
}
return default(T);
}
private void Initialize(object sender, EventArgs e)
{
if (!_connected)
Connect();
}
public async void Connect()
{
CancellationToken token = cancelToken.Token;
try
{
_connection = await ConnectInternal(token);
_commandConnection = await ConnectInternal(token);
}
catch(MpcNET.Exceptions.MpcConnectException exception)
{
Trace.WriteLine("exception: " + exception);
}
if (_connection != null && _commandConnection != null)
{
if (_connection.IsConnected && _commandConnection.IsConnected)
{
_connected = true;
_version = _connection.Version;
ConnectionChanged?.Invoke(this, EventArgs.Empty);
}
}
else
{
ConnectionChanged?.Invoke(this, EventArgs.Empty);
return;
if (_mpd.MpdStatus.MpdState == Status.MpdPlayState.Play)
await _mpd.MpdPlaybackPause();
else if (_mpd.MpdStatus.MpdState == Status.MpdPlayState.Pause)
await _mpd.MpdPlaybackPlay(_currentVolume);
}
public async void Random()
{
if (!IsBusy)
await _mpd.MpdSetRandom(!_currentRandom);
await UpdateStatusAsync();
await UpdateSongAsync();
Loop(token);
}
public async void Repeat()
private async Task<MpcConnection> ConnectInternal(CancellationToken token)
{
if (!IsBusy)
await _mpd.MpdSetRepeat(!_currentRepeat);
IPAddress.TryParse(Properties.Settings.Default.mpd_host, out IPAddress ipAddress);
_mpdEndpoint = new IPEndPoint(ipAddress, Properties.Settings.Default.mpd_port);
MpcConnection connection = new MpcConnection(_mpdEndpoint);
await connection.ConnectAsync(token);
/*if (!string.IsNullOrEmpty(Properties.Settings.Default.mpd_password))
{
IMpdMessage<string> result = await connection.SendAsync(new PasswordCommand(Properties.Settings.Default.mpd_password));
if (!result.IsResponseValid)
{
string mpdError = result.Response?.Result?.MpdError;
Trace.WriteLine(mpdError);
}
}*/
return connection;
}
public async void Single()
private void Disconnected()
{
if (!IsBusy)
await _mpd.MpdSetSingle(!_currentSingle);
_connected = false;
_connection = null;
_commandConnection = null;
ConnectionChanged?.Invoke(this, EventArgs.Empty);
}
public async void Consume()
private void Loop(CancellationToken token)
{
if (!IsBusy)
await _mpd.MpdSetConsume(!_currentConsume);
Task.Run(async () =>
{
while (true)
{
try
{
if (token.IsCancellationRequested || _connection == null)
break;
var idleChanges = await _connection.SendAsync(new IdleCommand("stored_playlist playlist player mixer output options"));
if (idleChanges.IsResponseValid)
await HandleIdleResponseAsync(idleChanges.Response.Content);
else
throw new Exception(idleChanges.Response?.Content);
}
catch (Exception e)
{
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
Disconnected();
}
}
public async void SetVolume(int value)
{
if (!IsBusy)
await _mpd.MpdSetVolume(value);
}).ConfigureAwait(false);
}
public bool IsPlaying()
private async Task HandleIdleResponseAsync(string subsystems)
{
return _currentStatus?.MpdState == MPDCtrl.Models.Status.MpdPlayState.Play;
try
{
if (subsystems.Contains("player") || subsystems.Contains("mixer") || subsystems.Contains("output") || subsystems.Contains("options"))
{
await UpdateStatusAsync();
if (subsystems.Contains("player"))
await UpdateSongAsync();
}
}
catch (Exception e)
{
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
}
}
private async Task UpdateStatusAsync()
{
if (_connection == null || _isUpdatingStatus)
return;
_isUpdatingStatus = true;
try
{
IMpdMessage<MpdStatus> response = await _connection.SendAsync(new StatusCommand());
if (response != null && response.IsResponseValid)
{
_currentStatus = response.Response.Content;
UpdateStatus();
}
else
throw new Exception();
}
catch (Exception e)
{
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
}
_isUpdatingStatus = false;
}
private async Task UpdateSongAsync()
{
if (_connection == null || _isUpdatingSong)
return;
_isUpdatingSong = true;
try
{
IMpdMessage<IMpdFile> response = await _connection.SendAsync(new CurrentSongCommand());
if (response != null && response.IsResponseValid)
{
_currentSong = response.Response.Content;
UpdateSong();
}
else
throw new Exception();
}
catch (Exception e)
{
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
}
_isUpdatingSong = false;
}
private async void GetAlbumCover(string path, CancellationToken token = default)
{
List<byte> data = new List<byte>();
try
{
if (_connection == null)
return;
long totalBinarySize = 9999;
long currentSize = 0;
do
{
var albumReq = await _connection.SendAsync(new AlbumArtCommand(path, currentSize));
if (!albumReq.IsResponseValid)
break;
var response = albumReq.Response.Content;
if (response.Binary == 0)
break;
totalBinarySize = response.Size;
currentSize += response.Binary;
data.AddRange(response.Data);
} while (currentSize < totalBinarySize && !token.IsCancellationRequested);
}
catch (Exception e)
{
Trace.WriteLine("Exception caught while getting albumart: " + e);
return;
}
if (data.Count == 0)
_cover = null;
else
{
using MemoryStream stream = new MemoryStream(data.ToArray());
_cover = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
UpdateCover();
}
public void UpdateStatus()
{
if (!_connected || _currentStatus == null)
return;
_currentRandom = _currentStatus.Random;
_currentRepeat = _currentStatus.Repeat;
_currentConsume = _currentStatus.Consume;
_currentSingle = _currentStatus.Single;
_currentVolume = _currentStatus.Volume;
StatusChanged?.Invoke(this, EventArgs.Empty);
}
public void UpdateSong()
{
if (!_connected || _currentSong == null)
return;
_currentTime = _currentStatus.Elapsed.TotalSeconds;
_totalTime = _currentSong.Time;
if (!_elapsedTimer.Enabled)
_elapsedTimer.Start();
SongChanged?.Invoke(this, EventArgs.Empty);
string uri = Regex.Escape(_currentSong.Path);
GetAlbumCover(uri);
}
public void UpdateCover()
{
CoverChanged?.Invoke(this, EventArgs.Empty);
}
public IMpdFile GetCurrentSong() => _currentSong;
public MpdStatus GetStatus() => _currentStatus;
public BitmapFrame GetCover() => _cover;
public string GetVersion() => _version;
public double GetCurrentTime() => _currentTime;
public bool IsConnected() => _connected;
public bool IsPlaying() => _currentStatus?.State == MpdState.Play;
public void Prev() => SendCommand(new PreviousCommand());
public void Next() => SendCommand(new NextCommand());
public void PlayPause() => SendCommand(new PauseResumeCommand());
public void Random() => SendCommand(new RandomCommand(!_currentRandom));
public void Repeat() => SendCommand(new RepeatCommand(!_currentRepeat));
public void Single() => SendCommand(new SingleCommand(!_currentSingle));
public void Consume() => SendCommand(new ConsumeCommand(!_currentConsume));
public void SetTime(double value) => SendCommand(new SeekCurCommand(value));
public void SetVolume(int value) => SendCommand(new SetVolumeCommand((byte)value));
public void VolumeUp()
{
_currentVolume += Properties.Settings.Default.volume_offset;
if (_currentVolume > 100)
_currentVolume = 100;
SetVolume(_currentVolume);
}
public void VolumeDown()
{
_currentVolume -= Properties.Settings.Default.volume_offset;
if (_currentVolume < 0)
_currentVolume = 0;
SetVolume(_currentVolume);
}
}
}

View File

@ -1,57 +1,68 @@
using System;
using System.Diagnostics;
using System.Windows;
using Hardcodet.Wpf.TaskbarNotification;
namespace unison
{
public class SnapcastHandler
{
private readonly Process _snapcast = new();
public bool Started { get; private set; }
private string _snapcastPath;
public bool HasStarted { get; private set; }
public SnapcastHandler()
public void OnConnectionChanged(object sender, EventArgs e)
{
// wip: this will have to be moved after the mpd connection, later on
_snapcastPath = Properties.Settings.Default.snapcast_path;
if (Properties.Settings.Default.snapcast_startup)
Start();
{
var mpd = (MPDHandler)Application.Current.Properties["mpd"];
if (mpd.IsConnected())
LaunchOrExit();
}
}
public void Start()
public void UpdateInterface()
{
if (!Started)
TaskbarIcon Systray = (TaskbarIcon)Application.Current.Properties["systray"];
SystrayViewModel DataContext = Systray.DataContext as SystrayViewModel;
DataContext.OnPropertyChanged("SnapcastText");
Application.Current.Dispatcher.Invoke(() =>
{
_snapcast.StartInfo.FileName = _snapcastPath + @"\snapclient.exe";
MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
MainWin.OnSnapcastChanged();
});
}
public void LaunchOrExit(bool ForceExit = false)
{
if (!HasStarted && !ForceExit)
{
_snapcast.StartInfo.FileName = Properties.Settings.Default.snapcast_path + @"\snapclient.exe";
_snapcast.StartInfo.Arguments = $"--host {Properties.Settings.Default.mpd_host}";
_snapcast.StartInfo.CreateNoWindow = true;
_snapcast.StartInfo.CreateNoWindow = !Properties.Settings.Default.snapcast_window;
try
{
_snapcast.Start();
}
catch (Exception err)
{
MessageBox.Show($"[Snapcast error]\nInvalid path: {err.Message}\n\nCurrent path: {_snapcastPath}\nYou can reset it in the settings if needed.",
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}",
"unison", MessageBoxButton.OK, MessageBoxImage.Error);
Trace.WriteLine(err.Message);
return;
}
Started = true;
HasStarted = true;
}
else
else if (HasStarted)
{
_snapcast.Kill();
Started = false;
}
HasStarted = false;
}
public void Stop()
{
if (Started)
{
_snapcast.Kill();
Started = false;
}
if (!ForceExit)
UpdateInterface();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace MPDCtrl.Models
{
/// <summary>
/// AlbumCover class.
/// </summary>
public class AlbumImage
{
public bool IsDownloading { get; set; }
public bool IsSuccess { get; set; }
public string SongFilePath { get; set; }
public byte[] BinaryData { get; set; } = Array.Empty<byte>();
public int BinarySize { get; set; }
public ImageSource AlbumImageSource { get; set; }
}
}

View File

@ -1,53 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MPDCtrl.Models
{
public class Playlist
{
public string Name { get; set; } = "";
private string _lastModified;
public string LastModified
{
get
{
return _lastModified;
}
set
{
if (_lastModified == value)
return;
_lastModified = value;
}
}
public string LastModifiedFormated
{
get
{
DateTime _lastModifiedDateTime = default; //new DateTime(1998,04,30)
if (!string.IsNullOrEmpty(_lastModified))
{
try
{
_lastModifiedDateTime = DateTime.Parse(_lastModified, null, System.Globalization.DateTimeStyles.RoundtripKind);
}
catch
{
System.Diagnostics.Debug.WriteLine("Wrong LastModified timestamp format. " + _lastModified);
}
}
var culture = System.Globalization.CultureInfo.CurrentCulture;
return _lastModifiedDateTime.ToString(culture);
}
}
}
}

View File

@ -1,52 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MPDCtrl.Models
{
public class Result
{
public bool IsSuccess;
public string ErrorMessage;
}
public class ConnectionResult: Result
{
}
// generic
public class CommandResult : Result
{
public string ResultText;
}
public class CommandBinaryResult : Result
{
public int WholeSize;
public int ChunkSize;
public string Type;
public byte[] BinaryData;
}
// for commands that return playlist songs.
public class CommandPlaylistResult : CommandResult
{
public ObservableCollection<SongInfo> PlaylistSongs;
}
// for commands that return search result.
public class CommandSearchResult : CommandResult
{
public ObservableCollection<SongInfo> SearchResult;
}
// TODO: Not used?
public class IdleResult : CommandResult
{
}
}

View File

@ -1,232 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
//using MPDCtrl.Common;
namespace MPDCtrl.Models
{
// SongFile > SongInfo > SongInfoEx
/// <summary>
/// Generic song file class. (for listall)
/// </summary>
public class SongFile// : ViewModelBase
{
public string File { get; set; } = "";
}
/// <summary>
/// SongInfo class. (for playlist or search result)
/// </summary>
public class SongInfo : SongFile
{
public string Title { get; set; } = "";
public string Track { get; set; } = "";
public string Disc { get; set; } = "";
public string Time { get; set; } = "";
public string TimeFormated
{
get
{
string _timeFormatted = "";
try
{
if (!string.IsNullOrEmpty(Time))
{
int sec, min, hour, s;
double dtime = double.Parse(Time);
sec = Convert.ToInt32(dtime);
//sec = Int32.Parse(_time);
min = sec / 60;
s = sec % 60;
hour = min / 60;
min %= 60;
if ((hour == 0) && min == 0)
{
_timeFormatted = String.Format("{0}", s);
}
else if ((hour == 0) && (min != 0))
{
_timeFormatted = String.Format("{0}:{1:00}", min, s);
}
else if ((hour != 0) && (min != 0))
{
_timeFormatted = String.Format("{0}:{1:00}:{2:00}", hour, min, s);
}
else if (hour != 0)
{
_timeFormatted = String.Format("{0}:{1:00}:{2:00}", hour, min, s);
}
else
{
System.Diagnostics.Debug.WriteLine("Oops@TimeFormated: " + Time + " : " + hour.ToString() + " " + min.ToString() + " " + s.ToString());
}
}
}
catch (FormatException e)
{
// Ignore.
// System.Diagnostics.Debug.WriteLine(e.Message);
System.Diagnostics.Debug.WriteLine("Wrong Time format. " + Time + " " + e.Message);
}
return _timeFormatted;
}
}
public double TimeSort
{
get
{
double dtime = double.NaN;
try
{
if (Time != "")
dtime = double.Parse(Time);
}
catch { }
return dtime;
}
}
public string Duration { get; set; } = "";
public string Artist { get; set; } = "";
public string Album { get; set; } = "";
public string AlbumArtist { get; set; } = "";
public string Composer { get; set; } = "";
public string Date { get; set; } = "";
public string Genre { get; set; } = "";
private string _lastModified;
public string LastModified
{
get
{
return _lastModified;
}
set
{
if (_lastModified == value)
return;
_lastModified = value;
}
}
public string LastModifiedFormated
{
get
{
DateTime _lastModifiedDateTime = default; //new DateTime(1998,04,30)
if (!string.IsNullOrEmpty(_lastModified))
{
try
{
_lastModifiedDateTime = DateTime.Parse(_lastModified, null, System.Globalization.DateTimeStyles.RoundtripKind);
}
catch
{
System.Diagnostics.Debug.WriteLine("Wrong LastModified timestamp format. " + _lastModified);
}
}
var culture = System.Globalization.CultureInfo.CurrentCulture;
return _lastModifiedDateTime.ToString(culture);
}
}
// for sorting and (playlist pos)
private int _index;
public int Index
{
get
{
return _index;
}
set
{
if (_index == value)
return;
_index = value;
//this.NotifyPropertyChanged(nameof(Index));
}
}
private bool _isSelected;
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
if (_isSelected == value)
return;
_isSelected = value;
//NotifyPropertyChanged("IsSelected");
}
}
public int IndexPlusOne
{
get
{
return _index+1;
}
}
}
/// <summary>
/// song class with some extra info. (for queue)
/// </summary>
public class SongInfoEx : SongInfo
{
// Queue specific
public string Id { get; set; }
private string _pos;
public string Pos
{
get
{
return _pos;
}
set
{
if (_pos == value)
return;
_pos = value;
//this.NotifyPropertyChanged(nameof(Pos));
}
}
private bool _isPlaying;
public bool IsPlaying
{
get
{
return _isPlaying;
}
set
{
if (_isPlaying == value)
return;
_isPlaying = value;
//this.NotifyPropertyChanged(nameof(IsPlaying));
}
}
}
}

View File

@ -1,149 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MPDCtrl.Models
{
/// <summary>
/// MPD "status" class. (for "status" command result)
/// </summary>
public class Status
{
public enum MpdPlayState
{
Play, Pause, Stop
};
private MpdPlayState _ps;
private string _bitrate;
private int _volume = 50;
private bool _volumeIsSet;
private bool _repeat;
private bool _random;
private bool _consume;
private bool _single;
private string _songID = "";
private double _songTime = 0;
private double _songElapsed = 0;
private string _error = "";
public MpdPlayState MpdState
{
get { return _ps; }
set { _ps = value; }
}
public string MpdBitrate
{
get { return _bitrate; }
set
{
_bitrate = value;
}
}
public int MpdVolume
{
get { return _volume; }
set
{
_volume = value;
}
}
public bool MpdVolumeIsSet
{
get { return _volumeIsSet; }
set
{
_volumeIsSet = value;
}
}
public bool MpdRepeat
{
get { return _repeat; }
set
{
_repeat = value;
}
}
public bool MpdRandom
{
get { return _random; }
set
{
_random = value;
}
}
public bool MpdConsume
{
get { return _consume; }
set
{
_consume = value;
}
}
public bool MpdSingle
{
get { return _single; }
set
{
_single = value;
}
}
public string MpdSongID
{
get { return _songID; }
set
{
_songID = value;
}
}
public double MpdSongTime
{
get { return _songTime; }
set
{
_songTime = value;
}
}
public double MpdSongElapsed
{
get { return _songElapsed; }
set
{
_songElapsed = value;
}
}
public string MpdError
{
get { return _error; }
set
{
_error = value;
}
}
public void Reset()
{
_volume = 50;
_volumeIsSet = false;
_repeat = false;
_random = false;
_consume = false;
_songID = "";
_songTime = 0;
_songElapsed = 0;
_error = "";
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -10,5 +10,8 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net5.0-windows</TargetFramework>
<SelfContained>false</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishSingleFile>True</PublishSingleFile>
<PublishReadyToRun>False</PublishReadyToRun>
</PropertyGroup>
</Project>

View File

@ -94,5 +94,29 @@ namespace unison.Properties {
this["snapcast_port"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("5")]
public int volume_offset {
get {
return ((int)(this["volume_offset"]));
}
set {
this["volume_offset"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
public bool snapcast_window {
get {
return ((bool)(this["snapcast_window"]));
}
set {
this["snapcast_window"] = value;
}
}
}
}

View File

@ -14,11 +14,17 @@
<Setting Name="snapcast_startup" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="snapcast_window" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="snapcast_path" Type="System.String" Scope="User">
<Value Profile="(Default)">snapclient_0.25.0-1_win64</Value>
</Setting>
<Setting Name="snapcast_port" Type="System.Int32" Scope="User">
<Value Profile="(Default)">1704</Value>
</Setting>
<Setting Name="volume_offset" Type="System.Int32" Scope="User">
<Value Profile="(Default)">5</Value>
</Setting>
</Settings>
</SettingsFile>

View File

@ -1,3 +1,44 @@
# unison
mpd client
![Main window](Screenshots/screen1.png)
**unison** is a very simple [Music Player Daemon (MPD)](https://www.musicpd.org/) daemon client with the following goals:
* lightweight window that can be toggled with shortcuts
* music control through shortcuts
* [Snapcast](https://mjaggard.github.io/snapcast/) integration
## Features
### Window
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)
### Shortcuts
You can control your music at anytime with the shortcuts. They can of course be used if the window is not visible.
![Settings => shortcuts](Screenshots/screen3.png)
### Snapcast
The main goal of embedding Snapcast is the ability to listen locally to music when I'm not using my main audio system. The computer running unison can then play music easily.
## Caveats
### Missing features
* Custom shortcuts.
* MPD passwords: I don't really see the point, but if asked, I will integrate them.
* Any sort of playlist and queue management. I use other software to do it, but I might implement them at some point.
### Wanted features
* A complete shuffle system based on set criteria, aka a smart playlist.
* Radio integration.
## Translations
unison is translated in English and French. You can contribute if you want!

423
Resources/Resources.Designer.cs generated Normal file
View File

@ -0,0 +1,423 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace unison.Resources {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// 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.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("unison.Resources.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Exit.
/// </summary>
public static string Exit {
get {
return ResourceManager.GetString("Exit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Settings.
/// </summary>
public static string Settings {
get {
return ResourceManager.GetString("Settings", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to About.
/// </summary>
public static string Settings_About {
get {
return ResourceManager.GetString("Settings_About", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to unison is a free software. It is built with the following technologies:.
/// </summary>
public static string Settings_AboutInfo {
get {
return ResourceManager.GetString("Settings_AboutInfo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connect.
/// </summary>
public static string Settings_ConnectButton {
get {
return ResourceManager.GetString("Settings_ConnectButton", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connection.
/// </summary>
public static string Settings_Connection {
get {
return ResourceManager.GetString("Settings_Connection", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connected to MPD.
/// </summary>
public static string Settings_ConnectionStatusConnected {
get {
return ResourceManager.GetString("Settings_ConnectionStatusConnected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connecting....
/// </summary>
public static string Settings_ConnectionStatusConnecting {
get {
return ResourceManager.GetString("Settings_ConnectionStatusConnecting", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Not connected..
/// </summary>
public static string Settings_ConnectionStatusOffline {
get {
return ResourceManager.GetString("Settings_ConnectionStatusOffline", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Host.
/// </summary>
public static string Settings_Host {
get {
return ResourceManager.GetString("Settings_Host", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to License.
/// </summary>
public static string Settings_License {
get {
return ResourceManager.GetString("Settings_License", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Made by.
/// </summary>
public static string Settings_MadeBy {
get {
return ResourceManager.GetString("Settings_MadeBy", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &apos;s updated MpcNET.
/// </summary>
public static string Settings_MpcNET {
get {
return ResourceManager.GetString("Settings_MpcNET", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Next track.
/// </summary>
public static string Settings_NextTrack {
get {
return ResourceManager.GetString("Settings_NextTrack", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Play / pause.
/// </summary>
public static string Settings_PlayPause {
get {
return ResourceManager.GetString("Settings_PlayPause", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Port.
/// </summary>
public static string Settings_Port {
get {
return ResourceManager.GetString("Settings_Port", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Previous track.
/// </summary>
public static string Settings_PreviousTrack {
get {
return ResourceManager.GetString("Settings_PreviousTrack", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Shortcuts.
/// </summary>
public static string Settings_Shortcuts {
get {
return ResourceManager.GetString("Settings_Shortcuts", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show window.
/// </summary>
public static string Settings_ShowWindow {
get {
return ResourceManager.GetString("Settings_ShowWindow", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You can change to your own locally installed version of the Snapcast client with an.
/// </summary>
public static string Settings_SnapcastInfo1 {
get {
return ResourceManager.GetString("Settings_SnapcastInfo1", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to absolute .
/// </summary>
public static string Settings_SnapcastInfo2 {
get {
return ResourceManager.GetString("Settings_SnapcastInfo2", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to path..
/// </summary>
public static string Settings_SnapcastInfo3 {
get {
return ResourceManager.GetString("Settings_SnapcastInfo3", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Launch at startup.
/// </summary>
public static string Settings_SnapcastLauch {
get {
return ResourceManager.GetString("Settings_SnapcastLauch", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Executable path.
/// </summary>
public static string Settings_SnapcastPath {
get {
return ResourceManager.GetString("Settings_SnapcastPath", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Port.
/// </summary>
public static string Settings_SnapcastPort {
get {
return ResourceManager.GetString("Settings_SnapcastPort", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Reset.
/// </summary>
public static string Settings_SnapcastResetButton {
get {
return ResourceManager.GetString("Settings_SnapcastResetButton", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show Snapcast window.
/// </summary>
public static string Settings_SnapcastWindow {
get {
return ResourceManager.GetString("Settings_SnapcastWindow", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Source code freely available.
/// </summary>
public static string Settings_SourceCode1 {
get {
return ResourceManager.GetString("Settings_SourceCode1", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to here.
/// </summary>
public static string Settings_SourceCode2 {
get {
return ResourceManager.GetString("Settings_SourceCode2", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Version:.
/// </summary>
public static string Settings_Version {
get {
return ResourceManager.GetString("Settings_Version", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Volume down.
/// </summary>
public static string Settings_VolumeDown {
get {
return ResourceManager.GetString("Settings_VolumeDown", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Volume offset.
/// </summary>
public static string Settings_VolumeOffset {
get {
return ResourceManager.GetString("Settings_VolumeOffset", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Volume up.
/// </summary>
public static string Settings_VolumeUp {
get {
return ResourceManager.GetString("Settings_VolumeUp", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show window.
/// </summary>
public static string ShowWindow {
get {
return ResourceManager.GetString("ShowWindow", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Snapcast error.
/// </summary>
public static string Snapcast_Popup1 {
get {
return ResourceManager.GetString("Snapcast_Popup1", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Invalid path:.
/// </summary>
public static string Snapcast_Popup2 {
get {
return ResourceManager.GetString("Snapcast_Popup2", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Current path:.
/// </summary>
public static string Snapcast_Popup3 {
get {
return ResourceManager.GetString("Snapcast_Popup3", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You can reset it in the settings if needed..
/// </summary>
public static string Snapcast_Popup4 {
get {
return ResourceManager.GetString("Snapcast_Popup4", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Start Snapcast.
/// </summary>
public static string StartSnapcast {
get {
return ResourceManager.GetString("StartSnapcast", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Stop Snapcast.
/// </summary>
public static string StopSnapcast {
get {
return ResourceManager.GetString("StopSnapcast", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,240 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Exit" xml:space="preserve">
<value>Quitter</value>
</data>
<data name="Settings" xml:space="preserve">
<value>Configuration</value>
</data>
<data name="Settings_About" xml:space="preserve">
<value>À propos</value>
</data>
<data name="Settings_AboutInfo" xml:space="preserve">
<value>unison est un logiciel libre. Il est développé avec les technologies suivantes :</value>
</data>
<data name="Settings_ConnectButton" xml:space="preserve">
<value>Connexion</value>
</data>
<data name="Settings_Connection" xml:space="preserve">
<value>Connexion</value>
</data>
<data name="Settings_ConnectionStatusConnected" xml:space="preserve">
<value>Connecté à MPD</value>
</data>
<data name="Settings_ConnectionStatusConnecting" xml:space="preserve">
<value>Connexion en cours...</value>
</data>
<data name="Settings_ConnectionStatusOffline" xml:space="preserve">
<value>Non connecté.</value>
</data>
<data name="Settings_Host" xml:space="preserve">
<value>Hôte</value>
</data>
<data name="Settings_License" xml:space="preserve">
<value>Licence</value>
</data>
<data name="Settings_MadeBy" xml:space="preserve">
<value>Créé par</value>
</data>
<data name="Settings_MpcNET" xml:space="preserve">
<value> et sa version de MpcNET</value>
</data>
<data name="Settings_NextTrack" xml:space="preserve">
<value>Piste suivante</value>
</data>
<data name="Settings_PlayPause" xml:space="preserve">
<value>Jouer / pause</value>
</data>
<data name="Settings_Port" xml:space="preserve">
<value>Port</value>
</data>
<data name="Settings_PreviousTrack" xml:space="preserve">
<value>Piste précédente</value>
</data>
<data name="Settings_Shortcuts" xml:space="preserve">
<value>Raccourcis</value>
</data>
<data name="Settings_ShowWindow" xml:space="preserve">
<value>Afficher la fenêtre</value>
</data>
<data name="Settings_SnapcastInfo1" xml:space="preserve">
<value>Il est possible de mettre votre version localement installé de Snapcast avec un </value>
</data>
<data name="Settings_SnapcastInfo2" xml:space="preserve">
<value>chemin absolu</value>
</data>
<data name="Settings_SnapcastInfo3" xml:space="preserve">
<value>.</value>
</data>
<data name="Settings_SnapcastLauch" xml:space="preserve">
<value>Lancer au démarrage</value>
</data>
<data name="Settings_SnapcastPath" xml:space="preserve">
<value>Chemin de l'exécutable</value>
</data>
<data name="Settings_SnapcastPort" xml:space="preserve">
<value>Port</value>
</data>
<data name="Settings_SnapcastResetButton" xml:space="preserve">
<value>Réinitialiser</value>
</data>
<data name="Settings_SnapcastWindow" xml:space="preserve">
<value>Afficher la fenêtre de Snapcast</value>
</data>
<data name="Settings_SourceCode1" xml:space="preserve">
<value>Code source librement disponible</value>
</data>
<data name="Settings_SourceCode2" xml:space="preserve">
<value>ici</value>
</data>
<data name="Settings_Version" xml:space="preserve">
<value>Version :</value>
</data>
<data name="Settings_VolumeDown" xml:space="preserve">
<value>Baisse de volume</value>
</data>
<data name="Settings_VolumeOffset" xml:space="preserve">
<value>Écart de volume</value>
</data>
<data name="Settings_VolumeUp" xml:space="preserve">
<value>Augmentation de volume</value>
</data>
<data name="ShowWindow" xml:space="preserve">
<value>Montrer la fenêtre</value>
</data>
<data name="Snapcast_Popup1" xml:space="preserve">
<value>Erreur Snapcast</value>
</data>
<data name="Snapcast_Popup2" xml:space="preserve">
<value>Chemin invalide :</value>
</data>
<data name="Snapcast_Popup3" xml:space="preserve">
<value>Chemin actuel :</value>
</data>
<data name="Snapcast_Popup4" xml:space="preserve">
<value>Vous pouvez le réinitialiser dans la configuration.</value>
</data>
<data name="StartSnapcast" xml:space="preserve">
<value>Démarrer Snapcast</value>
</data>
<data name="StopSnapcast" xml:space="preserve">
<value>Stopper Snapcast</value>
</data>
</root>

240
Resources/Resources.resx Normal file
View File

@ -0,0 +1,240 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Exit" xml:space="preserve">
<value>Exit</value>
</data>
<data name="Settings" xml:space="preserve">
<value>Settings</value>
</data>
<data name="Settings_About" xml:space="preserve">
<value>About</value>
</data>
<data name="Settings_AboutInfo" xml:space="preserve">
<value>unison is a free software. It is built with the following technologies:</value>
</data>
<data name="Settings_ConnectButton" xml:space="preserve">
<value>Connect</value>
</data>
<data name="Settings_Connection" xml:space="preserve">
<value>Connection</value>
</data>
<data name="Settings_ConnectionStatusConnected" xml:space="preserve">
<value>Connected to MPD</value>
</data>
<data name="Settings_ConnectionStatusConnecting" xml:space="preserve">
<value>Connecting...</value>
</data>
<data name="Settings_ConnectionStatusOffline" xml:space="preserve">
<value>Not connected.</value>
</data>
<data name="Settings_Host" xml:space="preserve">
<value>Host</value>
</data>
<data name="Settings_License" xml:space="preserve">
<value>License</value>
</data>
<data name="Settings_MadeBy" xml:space="preserve">
<value>Made by</value>
</data>
<data name="Settings_MpcNET" xml:space="preserve">
<value>'s updated MpcNET</value>
</data>
<data name="Settings_NextTrack" xml:space="preserve">
<value>Next track</value>
</data>
<data name="Settings_PlayPause" xml:space="preserve">
<value>Play / pause</value>
</data>
<data name="Settings_Port" xml:space="preserve">
<value>Port</value>
</data>
<data name="Settings_PreviousTrack" xml:space="preserve">
<value>Previous track</value>
</data>
<data name="Settings_Shortcuts" xml:space="preserve">
<value>Shortcuts</value>
</data>
<data name="Settings_ShowWindow" xml:space="preserve">
<value>Show window</value>
</data>
<data name="Settings_SnapcastInfo1" xml:space="preserve">
<value>You can change to your own locally installed version of the Snapcast client with an</value>
</data>
<data name="Settings_SnapcastInfo2" xml:space="preserve">
<value> absolute </value>
</data>
<data name="Settings_SnapcastInfo3" xml:space="preserve">
<value>path.</value>
</data>
<data name="Settings_SnapcastLauch" xml:space="preserve">
<value>Launch at startup</value>
</data>
<data name="Settings_SnapcastPath" xml:space="preserve">
<value>Executable path</value>
</data>
<data name="Settings_SnapcastPort" xml:space="preserve">
<value>Port</value>
</data>
<data name="Settings_SnapcastResetButton" xml:space="preserve">
<value>Reset</value>
</data>
<data name="Settings_SnapcastWindow" xml:space="preserve">
<value>Show Snapcast window</value>
</data>
<data name="Settings_SourceCode1" xml:space="preserve">
<value>Source code freely available</value>
</data>
<data name="Settings_SourceCode2" xml:space="preserve">
<value>here</value>
</data>
<data name="Settings_Version" xml:space="preserve">
<value>Version:</value>
</data>
<data name="Settings_VolumeDown" xml:space="preserve">
<value>Volume down</value>
</data>
<data name="Settings_VolumeOffset" xml:space="preserve">
<value>Volume offset</value>
</data>
<data name="Settings_VolumeUp" xml:space="preserve">
<value>Volume up</value>
</data>
<data name="ShowWindow" xml:space="preserve">
<value>Show window</value>
</data>
<data name="Snapcast_Popup1" xml:space="preserve">
<value>Snapcast error</value>
</data>
<data name="Snapcast_Popup2" xml:space="preserve">
<value>Invalid path:</value>
</data>
<data name="Snapcast_Popup3" xml:space="preserve">
<value>Current path:</value>
</data>
<data name="Snapcast_Popup4" xml:space="preserve">
<value>You can reset it in the settings if needed.</value>
</data>
<data name="StartSnapcast" xml:space="preserve">
<value>Start Snapcast</value>
</data>
<data name="StopSnapcast" xml:space="preserve">
<value>Stop Snapcast</value>
</data>
</root>

15
Resources/Resources.xaml Normal file
View File

@ -0,0 +1,15 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="snapcastPath">snapclient_0.25.0-1_win64</system:String>
<system:String x:Key="snapcastPort">1704</system:String>
<system:String x:Key="connectionOk1">&#xf385;</system:String>
<system:String x:Key="connectionOk2">&#xf386;</system:String>
<system:String x:Key="connectionFail">&#xf384;</system:String>
<system:String x:Key="playButton">&#xedb4;</system:String>
<system:String x:Key="pauseButton">&#xedb5;</system:String>
</ResourceDictionary>

BIN
Resources/icon-full.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
Resources/icon-mini.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

BIN
Screenshots/screen1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
Screenshots/screen2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
Screenshots/screen3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -4,13 +4,20 @@
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:clr="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:unison"
xmlns:properties="clr-namespace:unison.Resources"
mc:Ignorable="d"
Title="unison"
Closing="Window_Closing" Icon="/images/unison.ico" ResizeMode="CanMinimize" SizeToContent="WidthAndHeight">
Title="unison" Closing="Window_Closing" Icon="/Resources/icon-full.ico" ResizeMode="CanMinimize" SizeToContent="WidthAndHeight">
<Grid Background="{DynamicResource {x:Static SystemColors.ControlLightLightBrushKey}}" MinHeight="270">
<Window.Resources>
<Style TargetType="Border" x:Key="UnselectedButton">
<Setter Property="BorderThickness" Value="0, 0, 0, 0"/>
</Style>
<Style TargetType="Border" x:Key="SelectedButton">
<Setter Property="BorderThickness" Value="0, 0, 0, 2"/>
</Style>
</Window.Resources>
<Grid Background="{DynamicResource {x:Static SystemColors.ControlLightLightBrushKey}}" MinHeight="270" MinWidth="650">
<Grid x:Name="TopLayout" Margin="10,0,10,0" VerticalAlignment="Stretch" Width="Auto" Height="Auto">
<Grid x:Name="Display" HorizontalAlignment="Stretch" VerticalAlignment="Top" Margin="225,0,0,0" Height="Auto" Width="Auto">
<GroupBox Height="220" VerticalAlignment="Center">
@ -25,58 +32,61 @@
<TextBlock x:Name="SongTitle" TextWrapping="Wrap" TextAlignment="Center" FontWeight="Normal" FontSize="20" Text="Title"/>
<TextBlock x:Name="SongArtist" TextWrapping="Wrap" TextAlignment="Center" FontWeight="Bold" FontSize="18" Text="Artist"/>
<TextBlock x:Name="SongAlbum" TextWrapping="Wrap" TextAlignment="Center" FontWeight="Normal" FontSize="16" Text="Album"/>
<TextBlock x:Name="Bitrate" TextWrapping="Wrap" TextAlignment="Center" FontWeight="Normal" Text="Bitrate" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}"/>
<TextBlock x:Name="SongInfo" TextWrapping="Wrap" TextAlignment="Center" FontWeight="Normal" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" Margin="0,2,0,0">
<Run x:Name="SongGenre"/>
<Run x:Name="SongInfoDash"> </Run>
<Run x:Name="SongFormat"/>
</TextBlock>
</StackPanel>
</Grid>
<Grid x:Name="Controls" VerticalAlignment="Top" Margin="10,106,10,0">
<Grid x:Name="Controls" VerticalAlignment="Top" Margin="10,95,10,0">
<StackPanel HorizontalAlignment="Stretch" Orientation="Vertical" VerticalAlignment="Top">
<Grid HorizontalAlignment="Center" VerticalAlignment="Top">
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="CurrentTime" Text="0:00" TextWrapping="Wrap" HorizontalAlignment="Left"/>
<Slider x:Name="TimeSlider" MinWidth="320" Margin="5,0,5,0" HorizontalAlignment="Center" Maximum="100"/>
<Slider Style="{DynamicResource SliderTime}" x:Name="TimeSlider" MinWidth="320" Margin="5,0,5,0" HorizontalAlignment="Center" Maximum="100" PreviewMouseUp="TimeSlider_DragCompleted" Thumb.DragStarted="TimeSlider_DragStarted" FocusVisualStyle="{x:Null}"/>
<TextBlock x:Name="EndTime" Text="0:00" TextWrapping="Wrap" Height="18" HorizontalAlignment="Right"/>
</StackPanel>
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,5,0,0">
<Button x:Name="PreviousTrack" Click="Previous_Clicked" FontSize="18" Background="{x:Null}" BorderBrush="{x:Null}" FontWeight="Bold" HorizontalAlignment="Left">
<emoji:TextBlock ColorBlend="True" Text="⏪" FontSize="20"/>
<!-- https://docs.microsoft.com/en-us/windows/apps/design/style/segoe-ui-symbol-font -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,8,0,0">
<Button x:Name="PreviousTrack" Click="Previous_Clicked" Background="{x:Null}" BorderBrush="{x:Null}" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}">
<TextBlock FontSize="25" FontFamily="Segoe MDL2 Assets" Text="&#xeb9e;"/>
</Button>
<Button x:Name="PauseButton" Click="Pause_Clicked" FontSize="18" FontWeight="Bold" Background="{x:Null}" BorderBrush="{x:Null}" Margin="10,0,10,0">
<emoji:TextBlock x:Name="PauseButtonEmoji" ColorBlend="True" Text="⏯️" FontSize="20"/>
<Button x:Name="PauseButton" Click="Pause_Clicked" Background="{x:Null}" BorderBrush="{x:Null}" Margin="10,0,10,0" FocusVisualStyle="{x:Null}">
<TextBlock x:Name="PlayPause" FontSize="25" FontFamily="Segoe MDL2 Assets" Text="&#xedb5;"/>
</Button>
<Button x:Name="NextTrack" Click="Next_Clicked" FontSize="18" Background="{x:Null}" BorderBrush="{x:Null}" FontWeight="Bold" HorizontalAlignment="Right">
<emoji:TextBlock ColorBlend="True" Text="⏩" FontSize="20"/>
<Button x:Name="NextTrack" Click="Next_Clicked" FontSize="18" Background="{x:Null}" BorderBrush="{x:Null}" HorizontalAlignment="Right" FocusVisualStyle="{x:Null}">
<TextBlock FontSize="25" FontFamily="Segoe MDL2 Assets" Text="&#xeb9d;"/>
</Button>
</StackPanel>
<Grid VerticalAlignment="Stretch" Margin="0,2.5,0,0">
<StackPanel Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Center" Margin="0,6,0,0">
<TextBlock Text="🔈" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center"/>
<Slider x:Name="VolumeSlider" Maximum="100" Value="50" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" VerticalAlignment="Center" HorizontalAlignment="Center" MinWidth="180" FlowDirection="LeftToRight" Margin="5,0,5,0" Foreground="{x:Null}" Background="{x:Null}"/>
<TextBlock Text="🔊" TextWrapping="Wrap" HorizontalAlignment="Right" VerticalAlignment="Center"/>
<Grid VerticalAlignment="Stretch" Margin="0,18,0,0">
<StackPanel Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Center" Margin="10,2,10,0">
<TextBlock FontFamily="Segoe MDL2 Assets" Text="&#xe992;" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center" FontSize="16"/>
<Slider x:Name="VolumeSlider" Maximum="100" Value="50" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" VerticalAlignment="Center" HorizontalAlignment="Center" MinWidth="180" FlowDirection="LeftToRight" PreviewMouseUp="VolumeSlider_DragCompleted" FocusVisualStyle="{x:Null}"/>
<TextBlock FontFamily="Segoe MDL2 Assets" Text="&#xe995;" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="7.5,0,0,0" FontSize="16"/>
</StackPanel>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" VerticalAlignment="Bottom" MinHeight="35">
<Border x:Name="BorderRandom" BorderThickness="0,0,0,0" BorderBrush="Black" HorizontalAlignment="Stretch" VerticalAlignment="Top" SnapsToDevicePixels="True">
<Button x:Name="Random" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Background="{x:Null}" BorderBrush="{x:Null}" FontSize="18" Click="Random_Clicked">
<emoji:TextBlock ColorBlend="True" Text="🔀" FontSize="18" Margin="0" Padding="0, 0, 0, 2" />
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" VerticalAlignment="Bottom" MinHeight="27">
<Border x:Name="BorderRandom" Style="{StaticResource UnselectedButton}" BorderBrush="Black" HorizontalAlignment="Stretch" VerticalAlignment="Top" SnapsToDevicePixels="True">
<Button x:Name="Random" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Background="{x:Null}" BorderBrush="{x:Null}" Click="Random_Clicked" FocusVisualStyle="{x:Null}">
<TextBlock FontSize="18" FontFamily="Segoe MDL2 Assets" Text="&#xe8b1;" Margin="0" Padding="0, 0, 0, 2"/>
</Button>
</Border>
<Border x:Name="BorderRepeat" BorderThickness="0, 0, 0, 0" BorderBrush="Black" VerticalAlignment="Top" Margin="5,0,0,0">
<Button x:Name="Repeat" Background="{x:Null}" FontSize="18" BorderBrush="{x:Null}" Click="Repeat_Clicked">
<emoji:TextBlock ColorBlend="True" Text="🔁" FontSize="18" Margin="0" Padding="0, 0, 0, 2" />
<Border x:Name="BorderRepeat" Style="{StaticResource UnselectedButton}" BorderBrush="Black" VerticalAlignment="Top" Margin="10,0,0,0">
<Button x:Name="Repeat" Background="{x:Null}" FontSize="18" BorderBrush="{x:Null}" Click="Repeat_Clicked" FocusVisualStyle="{x:Null}">
<TextBlock FontSize="18" FontFamily="Segoe MDL2 Assets" Text="&#xe8ee;" Padding="0, 0, 0, 2"/>
</Button>
</Border>
</StackPanel>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" VerticalAlignment="Bottom" MinHeight="35">
<Border x:Name="BorderSingle" BorderThickness="0,0,0,0" BorderBrush="Black" HorizontalAlignment="Stretch" VerticalAlignment="Top" SnapsToDevicePixels="True" Margin="0,0,5,0">
<Button x:Name="Single" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="{x:Null}" Background="{x:Null}" Click="Single_Clicked">
<emoji:TextBlock ColorBlend="True" Text="🔂" FontSize="18" Margin="0" Padding="0, 0, 0, 2" TextDecorations="{x:Null}"/>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" VerticalAlignment="Bottom" MinHeight="27">
<Border x:Name="BorderSingle" Style="{StaticResource UnselectedButton}" BorderBrush="Black" HorizontalAlignment="Stretch" VerticalAlignment="Top" SnapsToDevicePixels="True" Margin="0,0,10,0">
<Button x:Name="Single" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="{x:Null}" Background="{x:Null}" Click="Single_Clicked" FocusVisualStyle="{x:Null}">
<TextBlock FontSize="18" FontFamily="Segoe MDL2 Assets" Text="&#xe8ed;" Margin="0" Padding="0, 0, 0, 2"/>
</Button>
</Border>
<Border x:Name="BorderConsume" BorderThickness="0,0,0,0" BorderBrush="Black" HorizontalAlignment="Stretch" VerticalAlignment="Top" SnapsToDevicePixels="True">
<Button x:Name="Consume" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="{x:Null}" Background="{x:Null}" Click="Consume_Clicked">
<emoji:TextBlock ColorBlend="True" Text="🆓" FontSize="18" Margin="0" Padding="0, 0, 0, 2"/>
<Border x:Name="BorderConsume" Style="{StaticResource UnselectedButton}" BorderBrush="Black" HorizontalAlignment="Stretch" VerticalAlignment="Top" SnapsToDevicePixels="True">
<Button x:Name="Consume" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="{x:Null}" Background="{x:Null}" Click="Consume_Clicked" FocusVisualStyle="{x:Null}">
<TextBlock FontSize="18" FontFamily="Segoe MDL2 Assets" Text="&#xe75c;" Margin="0" Padding="0, 0, 0, 2"/>
</Button>
</Border>
</StackPanel>
@ -93,31 +103,40 @@
<StackPanel.OpacityMask>
<VisualBrush Visual="{Binding ElementName=mask}"/>
</StackPanel.OpacityMask>
<Image x:Name="Cover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/images/nocover.png" Visibility="Collapsed" />
<Image x:Name="NoCover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/images/nocover.png" />
<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" />
</StackPanel>
</Grid>
</Border>
</Grid>
<Grid x:Name="BottomLayout" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Background="{DynamicResource {x:Static SystemColors.ControlLightBrushKey}}" Width="Auto" MinHeight="40">
<Button x:Name="Snapcast" HorizontalAlignment="Left" VerticalAlignment="Center" Click="Snapcast_Clicked" Margin="10,0,0,0" Padding="5, 2" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
<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 Orientation="Horizontal">
<emoji:TextBlock Text="🔊"/>
<TextBlock x:Name="SnapcastText" Text="Start Snapcast" 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>
<TextBlock x:Name="Connection" HorizontalAlignment="Center" Text="Not connected" TextWrapping="Wrap" VerticalAlignment="Center" TextAlignment="Center" MinWidth="350" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid x:Name="ConnectionOkIcon" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock FontFamily="Segoe MDL2 Assets" Text="&#xf385;" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" VerticalAlignment="Center" HorizontalAlignment="Center" />
<TextBlock FontFamily="Segoe MDL2 Assets" Text="&#xf386;" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Grid>
<Grid x:Name="ConnectionFailIcon" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock FontFamily="Segoe MDL2 Assets" Text="&#xf384;" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Grid>
<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}}">
<StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="🔁"/>
<emoji:TextBlock Text="🔁" Padding="0,0,0,2"/>
<TextBlock Text="Shuffle" Margin="5, 0, 0, 0"/>
</StackPanel>
</Button>-->
<Button x:Name="Settings" Padding="5, 2" Click="Settings_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
<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="🛠️"/>
<TextBlock Text="Settings" Margin="5, 0, 0, 0"/>
<emoji:TextBlock Text="🛠️" Padding="0,0,0,2"/>
<TextBlock Text="{x:Static properties:Resources.Settings}" Margin="5, 0, 0, 0"/>
</StackPanel>
</Button>
</StackPanel>

View File

@ -4,150 +4,247 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Windows.Interop;
using System.Windows.Input;
using System.Windows.Controls.Primitives;
namespace unison
{
public partial class MainWindow : Window, INotifyPropertyChanged
public partial class MainWindow : Window
{
private readonly Settings SettingsWindow = new Settings();
private MPDHandler mpd;
Thickness SelectedThickness;
Thickness BaseThickness;
private readonly Settings _settingsWin;
private readonly DispatcherTimer _timer;
private readonly MPDHandler _mpd;
public MainWindow()
{
InitHwnd();
InitializeComponent();
DefaultState(true);
WindowState = WindowState.Minimized;
mpd = (MPDHandler)Application.Current.Properties["mpd"];
_settingsWin = new Settings();
_timer = new DispatcherTimer();
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
DispatcherTimer timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(0.5);
timer.Tick += Timer_Tick;
timer.Start();
SelectedThickness.Left = SelectedThickness.Right = SelectedThickness.Top = 0.0f;
SelectedThickness.Bottom = 2.0f;
BaseThickness.Left = BaseThickness.Right = BaseThickness.Top = BaseThickness.Bottom = 0.0f;
_timer.Interval = TimeSpan.FromSeconds(0.5);
_timer.Tick += Timer_Tick;
_timer.Start();
}
private void Timer_Tick(object sender, EventArgs e)
{
UpdateInterface();
if (_mpd.GetCurrentSong() == null)
return;
CurrentTime.Text = FormatSeconds(_mpd.GetCurrentTime());
TimeSlider.Value = _mpd.GetCurrentTime() / _mpd.GetCurrentSong().Time * 100;
}
public void OnConnectionChanged(object sender, EventArgs e)
{
if (_mpd.IsConnected())
{
Snapcast.IsEnabled = true;
ConnectionOkIcon.Visibility = Visibility.Visible;
ConnectionFailIcon.Visibility = Visibility.Collapsed;
}
else
{
_timer.Stop();
DefaultState(true);
ConnectionOkIcon.Visibility = Visibility.Collapsed;
ConnectionFailIcon.Visibility = Visibility.Visible;
}
_settingsWin.UpdateConnectionStatus();
Connection.Text = $"{Properties.Settings.Default.mpd_host}:{Properties.Settings.Default.mpd_port}";
}
public void OnSongChanged(object sender, EventArgs e)
{
if (_mpd.GetCurrentSong() == null)
return;
if (_mpd.GetCurrentSong().HasTitle && _mpd.GetCurrentSong().Title.Length > 0)
SongTitle.Text = _mpd.GetCurrentSong().Title;
else if (_mpd.GetCurrentSong().HasName && _mpd.GetCurrentSong().Name.Length > 0)
SongTitle.Text = _mpd.GetCurrentSong().Name;
else
{
int start = _mpd.GetCurrentSong().Path.LastIndexOf("/") + 1;
int end = _mpd.GetCurrentSong().Path.LastIndexOf(".");
SongTitle.Text = _mpd.GetCurrentSong().Path.Substring(start, end - start);
}
SongTitle.ToolTip = _mpd.GetCurrentSong().Path;
SongArtist.Text = _mpd.GetCurrentSong().Artist;
SongAlbum.Text = _mpd.GetCurrentSong().Album;
if (_mpd.GetCurrentSong().Date != null)
SongAlbum.Text += $" ({ _mpd.GetCurrentSong().Date})";
SongGenre.Text = _mpd.GetCurrentSong().Genre;
SongFormat.Text = _mpd.GetCurrentSong().Path.Substring(_mpd.GetCurrentSong().Path.LastIndexOf(".") + 1);
if (SongGenre.Text.Length == 0 || SongFormat.Text.Length == 0)
SongInfoDash.Text = "";
else
SongInfoDash.Text = " ";
TimeSlider.IsEnabled = true;
if (_mpd.GetCurrentSong().Time == -1)
{
CurrentTime.Text = "";
EndTime.Text = "";
_timer.Stop();
TimeSlider.Value = 50;
TimeSlider.IsEnabled = false;
}
else
{
if (!_timer.IsEnabled)
_timer.Start();
EndTime.Text = FormatSeconds(_mpd.GetCurrentSong().Time);
}
}
public void OnStatusChanged(object sender, EventArgs e)
{
if (_mpd.GetStatus() == null)
return;
if (VolumeSlider.Value != _mpd.GetStatus().Volume)
{
VolumeSlider.Value = _mpd.GetStatus().Volume;
VolumeSlider.ToolTip = _mpd.GetStatus().Volume;
}
UpdateButton(ref BorderRandom, _mpd.GetStatus().Random);
UpdateButton(ref BorderRepeat, _mpd.GetStatus().Repeat);
UpdateButton(ref BorderSingle, _mpd.GetStatus().Single);
UpdateButton(ref BorderConsume, _mpd.GetStatus().Consume);
if (_mpd.IsPlaying())
PlayPause.Text = (string)Application.Current.FindResource("playButton");
else
{
PlayPause.Text = (string)Application.Current.FindResource("pauseButton");
if (_mpd.GetStatus().State == MpcNET.MpdState.Stop)
{
DefaultState();
}
}
}
private void DefaultState(bool LostConnection = false)
{
SongTitle.Text = "";
SongArtist.Text = "";
SongAlbum.Text = "";
SongGenre.Text = "";
SongInfoDash.Text = "";
SongFormat.Text = "";
CurrentTime.Text = "";
EndTime.Text = "";
PlayPause.Text = (string)Application.Current.FindResource("pauseButton");
TimeSlider.Value = 50;
TimeSlider.IsEnabled = false;
NoCover.Visibility = Visibility.Visible;
Cover.Visibility = Visibility.Collapsed;
if (LostConnection)
{
ConnectionOkIcon.Visibility = Visibility.Collapsed;
ConnectionFailIcon.Visibility = Visibility.Visible;
}
Connection.Text = $"{Properties.Settings.Default.mpd_host}:{Properties.Settings.Default.mpd_port}";
}
public void OnCoverChanged(object sender, EventArgs e)
{
if (_mpd.GetCover() == null)
{
NoCover.Visibility = Visibility.Visible;
Cover.Visibility = Visibility.Collapsed;
}
else if (Cover.Source != _mpd.GetCover())
{
Cover.Source = _mpd.GetCover();
Cover.Visibility = Visibility.Visible;
NoCover.Visibility = Visibility.Collapsed;
}
}
public void OnSnapcastChanged()
{
SnapcastHandler snapcast = (SnapcastHandler)Application.Current.Properties["snapcast"];
if (snapcast.HasStarted)
SnapcastText.Text = unison.Resources.Resources.StopSnapcast;
else
SnapcastText.Text = unison.Resources.Resources.StartSnapcast;
}
public void UpdateButton(ref Border border, bool b)
{
if (b)
border.BorderThickness = SelectedThickness;
else
border.BorderThickness = BaseThickness;
border.Style = b ? (Style)Resources["SelectedButton"] : (Style)Resources["UnselectedButton"];
}
public string FormatSeconds(int time)
{
TimeSpan timespan = TimeSpan.FromSeconds(time);
return timespan.ToString(@"mm\:ss");
}
public string FormatSeconds(double time)
{
TimeSpan timespan = TimeSpan.FromSeconds(time);
return timespan.ToString(@"mm\:ss");
}
public void UpdateInterface()
{
if (mpd.GetCurrentSong() != null && mpd.GetStatus() != null)
{
SongTitle.Text = mpd.GetCurrentSong().Title;
SongTitle.ToolTip = mpd.GetCurrentSong().File;
SongArtist.Text = mpd.GetCurrentSong().Artist;
SongAlbum.Text = mpd.GetCurrentSong().Album;
if (mpd.GetCurrentSong().Date.Length > 0)
SongAlbum.Text += $" ({ mpd.GetCurrentSong().Date})";
Bitrate.Text = mpd.GetCurrentSong().File.Substring(mpd.GetCurrentSong().File.LastIndexOf(".") + 1) + " ";
Bitrate.Text += mpd.GetStatus().MpdBitrate + "kbps";
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();
CurrentTime.Text = FormatSeconds(mpd._elapsed);
EndTime.Text = FormatSeconds(mpd.GetStatus().MpdSongTime);
if (!System.Double.IsNaN(mpd.GetCurrentSong().TimeSort))
TimeSlider.Value = mpd._elapsed / mpd.GetCurrentSong().TimeSort * 100;
}
if (VolumeSlider.Value != mpd._currentVolume)
{
VolumeSlider.Value = mpd._currentVolume;
VolumeSlider.ToolTip = mpd._currentVolume;
}
if (mpd.IsPlaying())
PauseButtonEmoji.Text = "⏸️";
else
PauseButtonEmoji.Text = "▶️";
SnapcastHandler snapcast = (SnapcastHandler)Application.Current.Properties["snapcast"];
if (snapcast.Started)
SnapcastText.Text = "Stop Snapcast";
else
SnapcastText.Text = "Start Snapcast";
Connection.Text = (mpd._connected ? "✔️" : "❌") + $"{Properties.Settings.Default.mpd_host}:{Properties.Settings.Default.mpd_port}";
UpdateButton(ref BorderRandom, mpd._currentRandom);
UpdateButton(ref BorderRepeat, mpd._currentRepeat);
UpdateButton(ref BorderSingle, mpd._currentSingle);
UpdateButton(ref BorderConsume, mpd._currentConsume);
if (mpd.GetCover() != null)
{
if ((!mpd.GetCover().IsDownloading) && mpd.GetCover().IsSuccess)
{
if (mpd.GetCurrentSong().File == mpd.GetCover().SongFilePath)
{
Cover.Source = mpd.GetCover().AlbumImageSource;
Cover.Visibility = Visibility.Visible;
NoCover.Visibility = Visibility.Collapsed;
return;
}
}
}
NoCover.Visibility = Visibility.Visible;
Cover.Visibility = Visibility.Collapsed;
}
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();
public void Consume_Clicked(object sender, RoutedEventArgs e) => mpd.Consume();
public void ChangeVolume(int value) => mpd.SetVolume(value);
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();
public void Consume_Clicked(object sender, RoutedEventArgs e) => _mpd.Consume();
public void ChangeVolume(int value) => _mpd.SetVolume(value);
public void Snapcast_Clicked(object sender, RoutedEventArgs e)
{
SnapcastHandler snapcast = (SnapcastHandler)Application.Current.Properties["snapcast"];
if (!snapcast.Started)
snapcast.Start();
else
snapcast.Stop();
snapcast.LaunchOrExit();
}
public void Settings_Clicked(object sender, RoutedEventArgs e)
{
SettingsWindow.Show();
SettingsWindow.Activate();
_settingsWin.Show();
_settingsWin.Activate();
if (SettingsWindow.WindowState == WindowState.Minimized)
SettingsWindow.WindowState = WindowState.Normal;
if (_settingsWin.WindowState == WindowState.Minimized)
_settingsWin.WindowState = WindowState.Normal;
}
private void Window_Closing(object sender, CancelEventArgs e)
private void TimeSlider_DragStarted(object sender, DragStartedEventArgs e)
{
e.Cancel = true;
WindowState = WindowState.Minimized;
Hide();
_timer.Stop();
}
private void TimeSlider_DragCompleted(object sender, MouseButtonEventArgs e)
{
Slider slider = (Slider)sender;
double SongPercentage = slider.Value;
double SongTime = _mpd.GetCurrentSong().Time;
double SeekTime = SongPercentage / 100 * SongTime;
_mpd.SetTime(SeekTime);
_timer.Start();
}
private void VolumeSlider_DragCompleted(object sender, MouseButtonEventArgs e)
{
Slider slider = (Slider)sender;
_mpd.SetVolume((int)slider.Value);
slider.ToolTip = (int)slider.Value;
}
protected override void OnSourceInitialized(EventArgs e)
@ -163,11 +260,11 @@ namespace unison
helper.EnsureHandle();
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
private void Window_Closing(object sender, CancelEventArgs e)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
e.Cancel = true;
WindowState = WindowState.Minimized;
Hide();
}
}
}

View File

@ -4,9 +4,9 @@
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:properties="clr-namespace:unison.Resources"
mc:Ignorable="d"
Closing="Window_Closing" Title="Settings" ResizeMode="CanMinimize" Icon="/images/unison.ico" WindowStyle="ToolWindow" SizeToContent="WidthAndHeight">
Closing="Window_Closing" Title="{x:Static properties:Resources.Settings}" ResizeMode="CanMinimize" Icon="/Resources/icon-full.ico" WindowStyle="ToolWindow" SizeToContent="WidthAndHeight">
<Grid>
<StackPanel Orientation="Vertical">
<TabControl Margin="10">
@ -15,26 +15,31 @@
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
<GroupBox.Header>
<StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="📶 Connection"/>
<TextBlock>
<emoji:EmojiInline Text="📶"/>
<Run Text="{x:Static properties:Resources.Settings_Connection}"/>
</TextBlock>
</StackPanel>
</GroupBox.Header>
<Grid VerticalAlignment="Top">
<StackPanel>
<StackPanel>
<TextBlock Text="Host" TextWrapping="Wrap" Margin="5,0,0,0"/>
<TextBlock Text="{x:Static properties:Resources.Settings_Host}" TextWrapping="Wrap" Margin="5,0,0,0"/>
<TextBox x:Name="MpdHost" TextWrapping="Wrap" Width="250" Margin="10,2,0,0"/>
</StackPanel>
<StackPanel Margin="0,5,0,0">
<TextBlock Text="Port" TextWrapping="Wrap" Margin="5,0,0,0"/>
<TextBlock Text="{x:Static properties:Resources.Settings_Port}" TextWrapping="Wrap" Margin="5,0,0,0"/>
<TextBox x:Name="MpdPort" MaxLength="5" PreviewTextInput="NumberValidationTextBox" TextWrapping="Wrap" Width="250" Margin="10,2,0,0"/>
</StackPanel>
<StackPanel Margin="0,5,0,0">
<!--<StackPanel Margin="0,5,0,0">
<TextBlock Text="Password" TextWrapping="Wrap" Margin="5,0,0,0"/>
<TextBox x:Name="MpdPassword" TextWrapping="Wrap" Width="250" Margin="10,2,0,0"/>
</StackPanel>
<Button Content="Connect" Margin="0,10,0,0" Width="120" Click="MPDConnect_Clicked"/>
</StackPanel>-->
<TextBlock x:Name="ConnectionStatus" Text="{x:Static properties:Resources.Settings_ConnectionStatusOffline}" TextWrapping="Wrap" Margin="5,10,0,0"/>
<Button x:Name="ConnectButton" Content="{x:Static properties:Resources.Settings_ConnectButton}" Margin="0,10,0,0" Width="120" Click="MPDConnect_Clicked"/>
</StackPanel>
</Grid>
</GroupBox>
@ -53,16 +58,19 @@
<StackPanel>
<StackPanel>
<CheckBox x:Name="SnapcastStartup" Margin="5, 5, 0, 0">
<TextBlock Text="Launch at startup" TextWrapping="Wrap"/>
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastLauch}" TextWrapping="Wrap"/>
</CheckBox>
<TextBlock Text="Port" TextWrapping="Wrap" Margin="5,5,0,0"/>
<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="Executable path" TextWrapping="Wrap" Margin="5,5,0,0"/>
<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">
You can change to your own locally installed version of the Snapcast client with an <Run FontStyle="Italic" FontWeight="DemiBold">absolute</Run> path.
<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="Reset" Margin="0,10,0,0" Width="120" Click="SnapcastReset_Clicked"/>
<Button Content="{x:Static properties:Resources.Settings_SnapcastResetButton}" Margin="0,10,0,0" Width="120" Click="SnapcastReset_Clicked"/>
</StackPanel>
</StackPanel>
</Grid>
@ -70,16 +78,24 @@
</DockPanel>
</TabItem>
<TabItem Header="Shortcuts">
<TabItem Header="{x:Static properties:Resources.Settings_Shortcuts}">
<DockPanel Margin="8">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
<GroupBox.Header>
<StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="⌨️ Shortcuts"/>
<TextBlock>
<emoji:EmojiInline Text="⌨️ "/>
<Run Text="{x:Static properties:Resources.Settings_Shortcuts}"></Run>
</TextBlock>
</StackPanel>
</GroupBox.Header>
<Grid>
<Grid MinWidth="220">
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{x:Static properties:Resources.Settings_VolumeOffset}" TextWrapping="Wrap"/>
<TextBox x:Name="VolumeOffset" TextWrapping="Wrap" Width="25" PreviewTextInput="NumberValidationTextBox" Margin="8,2,0,0"/>
</StackPanel>
<Grid MinWidth="300" Margin="0,5,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
@ -92,12 +108,12 @@
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Text="Next track" TextWrapping="Wrap" Grid.Column="0" Grid.Row="0" Margin="1"/>
<TextBlock Text="Previous track" TextWrapping="Wrap" Grid.Column="0" Grid.Row="1" Margin="1"/>
<TextBlock Text="Play / Pause" TextWrapping="Wrap" Grid.Column="0" Grid.Row="2" Margin="1"/>
<TextBlock Text="Volume up" TextWrapping="Wrap" Grid.Column="0" Grid.Row="3" Margin="1"/>
<TextBlock Text="Volume down" TextWrapping="Wrap" Grid.Column="0" Grid.Row="4" Margin="1"/>
<TextBlock Text="Show window" TextWrapping="Wrap" Grid.Column="0" Grid.Row="5" Margin="1"/>
<TextBlock Text="{x:Static properties:Resources.Settings_NextTrack}" TextWrapping="Wrap" Grid.Column="0" Grid.Row="0" Margin="1"/>
<TextBlock Text="{x:Static properties:Resources.Settings_PreviousTrack}" TextWrapping="Wrap" Grid.Column="0" Grid.Row="1" Margin="1"/>
<TextBlock Text="{x:Static properties:Resources.Settings_PlayPause}" TextWrapping="Wrap" Grid.Column="0" Grid.Row="2" Margin="1"/>
<TextBlock Text="{x:Static properties:Resources.Settings_VolumeUp}" TextWrapping="Wrap" Grid.Column="0" Grid.Row="3" Margin="1"/>
<TextBlock Text="{x:Static properties:Resources.Settings_VolumeDown}" TextWrapping="Wrap" Grid.Column="0" Grid.Row="4" Margin="1"/>
<TextBlock Text="{x:Static properties:Resources.Settings_ShowWindow}" TextWrapping="Wrap" Grid.Column="0" Grid.Row="5" Margin="1"/>
<TextBlock Text="ctrl + media_next" TextWrapping="Wrap" Grid.Column="1" Grid.Row="0" Margin="1" HorizontalAlignment="Right" FontWeight="Bold"/>
<TextBlock Text="ctrl + media_prev" TextWrapping="Wrap" Grid.Column="1" Grid.Row="1" Margin="1" HorizontalAlignment="Right" FontWeight="Bold"/>
@ -106,12 +122,13 @@
<TextBlock Text="ctrl + volume_down" TextWrapping="Wrap" Grid.Column="1" Grid.Row="4" Margin="1" HorizontalAlignment="Right" FontWeight="Bold"/>
<TextBlock Text="ctrl + alt + enter" TextWrapping="Wrap" Grid.Column="1" Grid.Row="5" Margin="1" HorizontalAlignment="Right" FontWeight="Bold"/>
</Grid>
</StackPanel>
</Grid>
</GroupBox>
</DockPanel>
</TabItem>
<TabItem Header="About" Height="20" VerticalAlignment="Bottom">
<TabItem Header="{x:Static properties:Resources.Settings_About}" Height="20" VerticalAlignment="Bottom">
<DockPanel Margin="8">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
<GroupBox.Header>
@ -120,23 +137,23 @@
<Grid VerticalAlignment="Top">
<StackPanel Orientation="Vertical">
<TextBlock TextWrapping="Wrap" Margin="0,0,0,10" VerticalAlignment="Top">
<Run Text="Version:"/>
<Run Text="{x:Static properties:Resources.Settings_Version}"/>
<Run Text="{Binding GetVersion, Mode = OneWay}"/>
</TextBlock>
<TextBlock TextWrapping="Wrap" VerticalAlignment="Top">
unison is free software. It is built with the following technologies:<LineBreak/>
※ <Hyperlink NavigateUri="https://torum.github.io/MPDCtrl/" RequestNavigate="Hyperlink_RequestNavigate">MPDCtrl</Hyperlink><LineBreak/>
<Run Text="{x:Static properties:Resources.Settings_AboutInfo}" /><LineBreak/>
※ <Hyperlink NavigateUri="https://github.com/Difegue/Stylophone" RequestNavigate="Hyperlink_RequestNavigate">Stylophone</Hyperlink><Run Text="{x:Static properties:Resources.Settings_MpcNET}" /><LineBreak/>
※ <Hyperlink NavigateUri="https://github.com/hardcodet/wpf-notifyicon" RequestNavigate="Hyperlink_RequestNavigate">wpf-notifyicon</Hyperlink><LineBreak/>
※ <Hyperlink NavigateUri="https://github.com/samhocevar/emoji.wpf" RequestNavigate="Hyperlink_RequestNavigate">Emoji.WPF</Hyperlink>
</TextBlock>
<TextBlock Margin="0,10,0,0">
Source code freely available
<Run Text="{x:Static properties:Resources.Settings_SourceCode1}" />
<Hyperlink NavigateUri="https://git.n700.ovh/keb/unison" RequestNavigate="Hyperlink_RequestNavigate">
here
<Run Text="{x:Static properties:Resources.Settings_SourceCode2}" />
</Hyperlink>.
</TextBlock>
<TextBlock Margin="0,10,0,0">
Made by
<Run Text="{x:Static properties:Resources.Settings_MadeBy}" />
<Hyperlink NavigateUri="https://marchal.dev" RequestNavigate="Hyperlink_RequestNavigate">
Théo Marchal
</Hyperlink>.
@ -147,7 +164,10 @@
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0" Margin="0,10,0,0">
<GroupBox.Header>
<emoji:TextBlock Text="📝 License"/>
<TextBlock>
<emoji:EmojiInline Text="📝 "/>
<Run Text="{x:Static properties:Resources.Settings_License}" />
</TextBlock>
</GroupBox.Header>
<Grid VerticalAlignment="Top">
<TextBlock Text="{Binding GetLicense, Mode = OneWay}" TextWrapping="Wrap" Width="500" TextAlignment="Justify" />

View File

@ -13,9 +13,6 @@ namespace unison
{
public partial class Settings : Window
{
private string defaultSnapcastPath = "snapclient_0.25.0-1_win64";
private string defaultSnapcastPort = "1704";
public static string GetVersion => Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
public static string GetLicense
@ -46,10 +43,12 @@ namespace unison
MpdHost.Text = Properties.Settings.Default.mpd_host;
MpdPort.Text = Properties.Settings.Default.mpd_port.ToString();
MpdPassword.Text = null; //Properties.Settings.Default.mpd_password;
//MpdPassword.Text = Properties.Settings.Default.mpd_password;
SnapcastStartup.IsChecked = Properties.Settings.Default.snapcast_startup;
SnapcastWindow.IsChecked = Properties.Settings.Default.snapcast_window;
SnapcastPath.Text = Properties.Settings.Default.snapcast_path;
SnapcastPort.Text = Properties.Settings.Default.snapcast_port.ToString();
VolumeOffset.Text = Properties.Settings.Default.volume_offset.ToString();
}
private void NumberValidationTextBox(object sender, TextCompositionEventArgs e)
@ -66,27 +65,39 @@ namespace unison
e.Handled = true;
}
public void UpdateConnectionStatus()
{
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
if (mpd.IsConnected())
ConnectionStatus.Text = $"{unison.Resources.Resources.Settings_ConnectionStatusConnected} {mpd.GetVersion()}.";
else
ConnectionStatus.Text = unison.Resources.Resources.Settings_ConnectionStatusOffline;
}
private void MPDConnect_Clicked(object sender, RoutedEventArgs e)
{
SaveSettings();
ConnectionStatus.Text = unison.Resources.Resources.Settings_ConnectionStatusConnecting;
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
//mpd.Connect();
mpd.Connect();
}
private void SnapcastReset_Clicked(object sender, RoutedEventArgs e)
{
SnapcastPath.Text = defaultSnapcastPath;
SnapcastPort.Text = defaultSnapcastPort;
SnapcastPath.Text = (string)Application.Current.FindResource("snapcastPath");
SnapcastPort.Text = (string)Application.Current.FindResource("snapcastPort");
}
public void SaveSettings()
{
Properties.Settings.Default.mpd_host = MpdHost.Text;
Properties.Settings.Default.mpd_port = int.Parse(MpdPort.Text, CultureInfo.InvariantCulture);
Properties.Settings.Default.mpd_password = null;//MpdPassword.Text;
//Properties.Settings.Default.mpd_password = MpdPassword.Text;
Properties.Settings.Default.snapcast_startup = (bool)SnapcastStartup.IsChecked;
Properties.Settings.Default.snapcast_window = (bool)SnapcastWindow.IsChecked;
Properties.Settings.Default.snapcast_path = SnapcastPath.Text;
Properties.Settings.Default.snapcast_port = int.Parse(SnapcastPort.Text, CultureInfo.InvariantCulture);
Properties.Settings.Default.volume_offset = int.Parse(VolumeOffset.Text, CultureInfo.InvariantCulture);
Properties.Settings.Default.Save();
}

View File

@ -2,24 +2,25 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tb="http://www.hardcodet.net/taskbar"
xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf"
xmlns:properties="clr-namespace:unison.Resources"
xmlns:local="clr-namespace:unison">
<ContextMenu x:Shared="false" x:Key="SystrayMenu">
<MenuItem IsEnabled="False">
<MenuItem.Icon>
<Image Source="/images/unison.ico" Width="16" Height="16"/>
<Image Source="/Resources/icon-full.ico" Width="16" Height="16"/>
</MenuItem.Icon>
<MenuItem.Header>
<TextBlock Text="{Binding GetAppText}" />
</MenuItem.Header>
</MenuItem>
<Separator />
<MenuItem Header="Show window" Command="{Binding ShowWindowCommand}">
<MenuItem Header="{x:Static properties:Resources.ShowWindow}" Command="{Binding ShowWindowCommand}">
<MenuItem.Icon>
<Image Width="16" Height="16" emoji:Image.Source="▶️" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Name="test_snapcast_item" Header="{Binding SnapcastText}" Command="{Binding Snapcast}">
<MenuItem Header="{Binding SnapcastText}" Command="{Binding Snapcast}">
<MenuItem.Icon>
<Image Width="16" Height="16" emoji:Image.Source="🔊" />
</MenuItem.Icon>
@ -29,20 +30,20 @@
<Image Width="16" Height="16" emoji:Image.Source="🔀" />
</MenuItem.Icon>
</MenuItem>-->
<MenuItem Header="Settings" Command="{Binding Settings}">
<MenuItem Header="{x:Static properties:Resources.Settings}" Command="{Binding Settings}">
<MenuItem.Icon>
<Image Width="16" Height="16" emoji:Image.Source="🛠️" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Exit" Command="{Binding ExitApplicationCommand}">
<MenuItem Header="{x:Static properties:Resources.Exit}" Command="{Binding ExitApplicationCommand}">
<MenuItem.Icon>
<Image Width="16" Height="16" emoji:Image.Source="❌" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
<tb:TaskbarIcon x:Key="SystrayTaskbar" IconSource="/images/unison.ico" ToolTipText="{Binding GetAppText}" DoubleClickCommand="{Binding ShowWindowCommand}" ContextMenu="{StaticResource SystrayMenu}">
<tb:TaskbarIcon x:Key="SystrayTaskbar" IconSource="/Resources/icon-mini.ico" ToolTipText="{Binding GetAppText}" DoubleClickCommand="{Binding ShowWindowCommand}" ContextMenu="{StaticResource SystrayMenu}">
<tb:TaskbarIcon.DataContext>
<local:SystrayViewModel />
</tb:TaskbarIcon.DataContext>

View File

@ -8,6 +8,8 @@ namespace unison
{
public class SystrayViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public static string GetAppText => "unison v" + Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
public static ICommand ShowWindowCommand => new DelegateCommand
@ -26,19 +28,16 @@ namespace unison
public static ICommand ExitApplicationCommand => new DelegateCommand
{
CommandAction = () =>
{
Application.Current.Shutdown();
},
CommandAction = () => Application.Current.Shutdown(),
CanExecuteFunc = () => true
};
public static string SnapcastText
public string SnapcastText
{
get
{
SnapcastHandler snapcast = (SnapcastHandler)Application.Current.Properties["snapcast"];
return snapcast.Started ? "Stop Snapcast" : "Start Snapcast";
return snapcast.HasStarted ? unison.Resources.Resources.StopSnapcast : unison.Resources.Resources.StartSnapcast;
}
}
@ -51,7 +50,9 @@ namespace unison
CommandAction = () =>
{
Application.Current.Dispatcher.BeginInvoke(new Action(() => OnPropertyChanged("SnapcastText")));
((MainWindow)Application.Current.MainWindow).Snapcast_Clicked(null, null);
SnapcastHandler snapcast = (SnapcastHandler)Application.Current.Properties["snapcast"];
snapcast.LaunchOrExit();
},
CanExecuteFunc = () => true
};
@ -64,18 +65,13 @@ namespace unison
{
return new DelegateCommand
{
CommandAction = () =>
{
((MainWindow)Application.Current.MainWindow).Settings_Clicked(null, null);
},
CommandAction = () => ((MainWindow)Application.Current.MainWindow).Settings_Clicked(null, null),
CanExecuteFunc = () => true
};
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
public virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -4,15 +4,23 @@
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<ApplicationIcon>images\unison.ico</ApplicationIcon>
<ApplicationIcon>Resources\icon-full.ico</ApplicationIcon>
<Win32Resource></Win32Resource>
<StartupObject>unison.App</StartupObject>
<Version>0.0.1</Version>
<Version>1.0</Version>
<Company />
<Authors>Théo Marchal</Authors>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://git.n700.ovh/keb/unison</PackageProjectUrl>
<RepositoryUrl>https://git.n700.ovh/keb/unison</RepositoryUrl>
<Copyright>Théo Marchal</Copyright>
<PackageIconUrl />
</PropertyGroup>
<ItemGroup>
<None Remove="images\nocover-test.png" />
<None Remove="images\unison.ico" />
<None Remove="Resources\icon-full.ico" />
<None Remove="Resources\icon-mini.ico" />
<None Remove="Resources\nocover.png" />
<None Remove="LICENSE" />
<None Remove="snapclient_0.25.0-1_win64\FLAC.dll" />
<None Remove="snapclient_0.25.0-1_win64\ogg.dll" />
@ -21,10 +29,20 @@
<None Remove="snapclient_0.25.0-1_win64\snapclient.exe" />
<None Remove="snapclient_0.25.0-1_win64\soxr.dll" />
<None Remove="snapclient_0.25.0-1_win64\vorbis.dll" />
<None Include="LICENSE">
<Pack>True</Pack>
<PackagePath></PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<Resource Include="images\nocover.png">
<Resource Include="Resources\icon-full.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\icon-mini.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\nocover.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Content Include="LICENSE">
@ -33,9 +51,6 @@
<Content Include="snapclient_0.25.0-1_win64\README.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Resource Include="images\unison.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Content Include="snapclient_0.25.0-1_win64\FLAC.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@ -61,12 +76,31 @@
<PackageReference Include="Hardcodet.NotifyIcon.Wpf" Version="1.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MpcNET\MpcNET.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Settings.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
<Compile Update="Resources\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources\Resources.fr-FR.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>

View File

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