389 lines
14 KiB
C#
389 lines
14 KiB
C#
using System;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading.Tasks;
|
|
using System.Timers;
|
|
using System.Windows;
|
|
using System.Windows.Media.Imaging;
|
|
using libplaza;
|
|
using ManagedBass;
|
|
using Meziantou.Framework.Win32;
|
|
|
|
namespace NightwaveForWorkgroups {
|
|
public partial class MainWindow : Window {
|
|
#region DLL Imports
|
|
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Ansi)]
|
|
private static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)]string lpFileName);
|
|
#endregion
|
|
#region StateVars
|
|
private static readonly object BufferLock=new object();
|
|
private int BuffCh;
|
|
private int BufferReqs;
|
|
private int MuteVol=-1;
|
|
private int GfxWidthStep=-1;
|
|
private string LastArtwork="";
|
|
private readonly Nightwave NwP=new Nightwave();
|
|
private Timer tRefresh;
|
|
private Timer tAudio;
|
|
private Timer tGfx;
|
|
private Timer tLogon;
|
|
private bool AudioInit=false;
|
|
#endregion
|
|
#region Delegates
|
|
private delegate void dgtNoParam();
|
|
private delegate void dgtBool(bool b);
|
|
private delegate void dgtStr(string s);
|
|
private delegate void dgtCtrl(System.Windows.Controls.Control c);
|
|
#endregion
|
|
#region CrossThreadCalls
|
|
private void Leveller() {
|
|
if(GfxWidthStep < 0)
|
|
GfxWidthStep = 32768 / (int)chLeftCvs.ActualWidth;
|
|
chLeft.Width = Bass.ChannelGetLevelLeft(BuffCh) / GfxWidthStep;
|
|
chRight.Width = Bass.ChannelGetLevelRight(BuffCh) / GfxWidthStep;
|
|
}
|
|
private void DgtToggleEnabled(System.Windows.Controls.Control c) =>
|
|
c.IsEnabled = !c.IsEnabled;
|
|
private void DgtStatus(string s) =>
|
|
sbStatus.Text = s;
|
|
private void DgtTitle(string s) =>
|
|
Title = s;
|
|
private void SetStatus(string msg) =>
|
|
Dispatcher.BeginInvoke(new dgtStr(DgtStatus), msg);
|
|
private void SetTitle(string msg) =>
|
|
Dispatcher.BeginInvoke(new dgtStr(DgtTitle), msg);
|
|
private async void DgtStartupLogon() {
|
|
Credential c=CredentialManager.ReadCredential("Nightwave.NET");
|
|
if(c is null)
|
|
return;
|
|
try {
|
|
if(await NwP.Login(c.UserName, c.Password) != true)
|
|
return;
|
|
} catch(Exception) {
|
|
return;
|
|
}
|
|
btLogin.IsEnabled = false;
|
|
btLogin.Visibility = Visibility.Hidden;
|
|
imLogin.IsEnabled = false;
|
|
imLogin.Visibility = Visibility.Hidden;
|
|
btDislike.IsEnabled = true;
|
|
btLike.IsEnabled = true;
|
|
SetStatus($"Logged in as {await NwP.GetUser()}");
|
|
}
|
|
private async void DgtRefresh(bool Force = false) {
|
|
GfxWidthStep = 32768 / (int)chLeftCvs.ActualWidth;
|
|
Status s;
|
|
try {
|
|
s = await NwP.Status(Force);
|
|
} catch(NullReferenceException) {
|
|
SetStatus("Ran into a problem while fetching now-playing.");
|
|
return;
|
|
}
|
|
if(s.FaultOccurred || s.InMaintenance) {
|
|
if(s.InMaintenance)
|
|
SetStatus("Nightwave Plaza is currently in maintenance mode.");
|
|
else
|
|
SetStatus("Ran into a problem while fetching now-playing.");
|
|
return;
|
|
}
|
|
slDuration.Value = s.CalculatedElapsed;
|
|
sbListeners.Text = $"{s.Listeners} listeners";
|
|
lbLikeCt.Content = s.Likes;
|
|
lbDislikeCt.Content = s.Dislikes;
|
|
lbElapsed.Content = $"{(s.CalculatedElapsed / 60).ToString("D")}:{(s.CalculatedElapsed % 60).ToString("D2")}";
|
|
if(LastArtwork != s.ArtworkUri || Force) {
|
|
//TODO should probably break this part out into a separate function cause this feels real messy
|
|
Vote v = await NwP.Vote() ?? Vote.Neutral;
|
|
switch(v) {
|
|
case Vote.Dislike:
|
|
sbVote.Text = "👎";
|
|
sbVote.ToolTip = "You dislike this.";
|
|
btLike.Content = "👍";
|
|
btLike.ToolTip = "Like this track";
|
|
btDislike.Content = "🤷♀️";
|
|
btDislike.ToolTip = "Remove your vote for this track";
|
|
break;
|
|
case Vote.Like:
|
|
sbVote.Text = "👍";
|
|
sbVote.ToolTip = "You like this.";
|
|
btLike.Content = "💖";
|
|
btLike.ToolTip = "Favourite this track";
|
|
btDislike.Content = "👎";
|
|
btDislike.ToolTip = "Dislike this track";
|
|
break;
|
|
case Vote.Favourite:
|
|
sbVote.Text = "💖";
|
|
sbVote.ToolTip = "You have favourited this.";
|
|
btLike.Content = "🤷♀️";
|
|
btLike.ToolTip = "Remove your vote for this track";
|
|
btDislike.Content = "👎";
|
|
btDislike.ToolTip = "Dislike this track";
|
|
break;
|
|
default:
|
|
sbVote.Text = "🤷♀️";
|
|
sbVote.ToolTip = "You have no feelings towards this, or you have not expressed your feelings for this.";
|
|
btLike.Content = "👍";
|
|
btLike.ToolTip = "Like this track";
|
|
btDislike.Content = "👎";
|
|
btDislike.ToolTip = "Dislike this track";
|
|
break;
|
|
}
|
|
//TODO visual effect on the like/dislike buttons to indicate user's vote
|
|
lbArtist.Content = s.Artist;
|
|
lbTitle.Content = s.Title;
|
|
lbAlbum.Content = s.Album;
|
|
if(btPlayPause.IsChecked == true)
|
|
SetTitle($"{s.Title} - {s.Artist}");
|
|
slDuration.Maximum = s.Duration;
|
|
lbTime.Content = $"{(s.Duration / 60).ToString("D")}:{(s.Duration % 60).ToString("D2")}";
|
|
if(LastArtwork != s.ArtworkUri) {
|
|
art.Source = new BitmapImage(new Uri(s.ArtworkUri));
|
|
LastArtwork = s.ArtworkUri;
|
|
}
|
|
SetStatus("");
|
|
}
|
|
}
|
|
private void DgtAudio() {
|
|
long progress = Bass.StreamGetFilePosition(BuffCh, FileStreamPosition.Buffer)
|
|
* 100 / Bass.StreamGetFilePosition(BuffCh, FileStreamPosition.End);
|
|
if(progress > 75 || Bass.StreamGetFilePosition(BuffCh, FileStreamPosition.Connected) == 0) {
|
|
tAudio.Stop();
|
|
SetStatus("Playing");
|
|
SetTitle($"{lbTitle.Content} - {lbArtist.Content}");
|
|
tGfx.Start();
|
|
_ = Bass.ChannelSetSync(BuffCh, SyncFlags.MetadataReceived, 0, MetaSync);
|
|
_ = Bass.ChannelSetSync(BuffCh, SyncFlags.OggChange, 0, MetaSync);
|
|
_ = Bass.ChannelSetSync(BuffCh, SyncFlags.End, 0, EndSync);
|
|
_ = Bass.ChannelPlay(BuffCh);
|
|
} else
|
|
SetStatus($"Buffering... {progress}%");
|
|
}
|
|
#endregion
|
|
#region PlaybackControllers
|
|
private void LoadUri(string uri) => Task.Factory.StartNew(() => {
|
|
int r;
|
|
lock(BufferLock)
|
|
r = ++BufferReqs;
|
|
tAudio.Stop();
|
|
_ = Bass.StreamFree(BuffCh);
|
|
SetStatus("Connecting stream..");
|
|
int stream = Bass.CreateStream(uri, 0, BassFlags.StreamDownloadBlocks | BassFlags.StreamStatus | BassFlags.AutoFree, StatusProc, new IntPtr(r));
|
|
lock(BufferLock) {
|
|
if(r != BufferReqs) {
|
|
if(stream != 0)
|
|
_ = Bass.StreamFree(stream);
|
|
}
|
|
BuffCh = stream;
|
|
}
|
|
if(BuffCh == 0)
|
|
SetStatus("Unable to connect to stream");
|
|
else
|
|
tAudio.Start();
|
|
});
|
|
private void PlazaLoad() =>
|
|
LoadUri("https://radio.plaza.one/ogg");
|
|
private void PlazaStop() {
|
|
tGfx.Stop();
|
|
lock(BufferLock) {
|
|
_ = Bass.StreamFree(BufferReqs);
|
|
_ = Bass.StreamFree(BuffCh);
|
|
_ = Bass.ChannelStop(BufferReqs);
|
|
_ = Bass.ChannelStop(BuffCh);
|
|
tAudio.Stop();
|
|
SetStatus("Idle..");
|
|
SetTitle("Nightwave.Net");
|
|
}
|
|
}
|
|
#endregion
|
|
#region EventHandlers
|
|
private void EndSync(int hdl, int ch, int dat, IntPtr usr) {
|
|
SetStatus("Idle..");
|
|
SetTitle("Nightwave.Net");
|
|
}
|
|
private void MetaSync(int hdl, int ch, int dat, IntPtr usr) =>
|
|
Dispatcher.BeginInvoke(new dgtBool(DgtRefresh), true);
|
|
private void StatusProc(IntPtr buf, int len, IntPtr usr) {
|
|
if(buf != IntPtr.Zero && len == 0 && usr.ToInt32() == BufferReqs)
|
|
SetStatus(Marshal.PtrToStringAnsi(buf));
|
|
}
|
|
private async void EvtRefresh(object sender, ElapsedEventArgs e) =>
|
|
await Dispatcher.BeginInvoke(new dgtBool(DgtRefresh), false);
|
|
private async void EvtLogon(object sender, ElapsedEventArgs e) =>
|
|
await Dispatcher.BeginInvoke(new dgtNoParam(DgtStartupLogon));
|
|
private void EvtAudio(object sender, ElapsedEventArgs e) =>
|
|
_ = Dispatcher.BeginInvoke(new dgtNoParam(DgtAudio));
|
|
private async void EvtRender(object sender, ElapsedEventArgs e) =>
|
|
await Dispatcher.BeginInvoke(new dgtNoParam(Leveller));
|
|
private void BtPlayPause_Click(object sender, RoutedEventArgs e) {
|
|
if(!AudioInit)
|
|
return;
|
|
_ = Dispatcher.BeginInvoke(new dgtCtrl(DgtToggleEnabled), btPlayPause);
|
|
_ = btPlayPause.IsChecked == true
|
|
? Dispatcher.BeginInvoke(new dgtNoParam(PlazaLoad))
|
|
: Dispatcher.BeginInvoke(new dgtNoParam(PlazaStop));
|
|
_ = Dispatcher.BeginInvoke(new dgtCtrl(DgtToggleEnabled), btPlayPause);
|
|
}
|
|
private void SbMute_Click(object sender, RoutedEventArgs e) {
|
|
if(MuteVol < 0) {
|
|
MuteVol = Bass.GlobalStreamVolume;
|
|
Bass.GlobalStreamVolume = 0;
|
|
sbMute.Content = "🔇";
|
|
tbMute.ImageSource = new BitmapImage(new Uri("pack://application:,,,/;component/Resources/muted.png"));
|
|
} else {
|
|
Bass.GlobalStreamVolume = MuteVol;
|
|
MuteVol = -1;
|
|
sbMute.Content = "🔊";
|
|
tbMute.ImageSource = new BitmapImage(new Uri("pack://application:,,,/;component/Resources/unmuted.png"));
|
|
}
|
|
}
|
|
private void SbVol_MouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e) {
|
|
sbVol.Value += e.Delta / 25;
|
|
e.Handled = true;
|
|
}
|
|
private void SbVol_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) {
|
|
if(!AudioInit)
|
|
return;
|
|
if(MuteVol >= 0 && sbMute.IsChecked == true)
|
|
sbMute.IsChecked = false;
|
|
Bass.GlobalStreamVolume = (int)sbVol.Value * 100;
|
|
}
|
|
private async void BtLogin_Click(object sender, RoutedEventArgs e) {
|
|
_ = Dispatcher.BeginInvoke(new dgtCtrl(DgtToggleEnabled), btLogin);
|
|
CredentialResult _c=CredentialManager.PromptForCredentials(
|
|
messageText:"Please log into your Nightwave Plaza account",
|
|
captionText:"Log into plaza.one");
|
|
if(_c is null)
|
|
return;
|
|
if(!await NwP.Login(_c.UserName, _c.Password)) {
|
|
SetStatus("Login failed! Please try again.");
|
|
_ = Dispatcher.BeginInvoke(new dgtCtrl(DgtToggleEnabled), btLogin);
|
|
return;
|
|
}
|
|
//user may have stored credentials and not want to keep them any more, delete with reckless abandon.
|
|
CredentialManager.DeleteCredential("Nightwave.NET");
|
|
if(_c.CredentialSaved == CredentialSaveOption.Selected) {
|
|
CredentialManager.WriteCredential("Nightwave.NET", _c.UserName, _c.Password, CredentialPersistence.Enterprise);
|
|
Credential c=CredentialManager.ReadCredential("Nightwave.NET");
|
|
if(c is null) {
|
|
SetStatus("Problem saving to Windows Credential store. Sorry.");
|
|
throw new Exception("what the fuck");
|
|
}
|
|
}
|
|
if(!await NwP.CheckSession()) {
|
|
SetStatus("Something went wrong during login! Sorry.");
|
|
_ = Dispatcher.BeginInvoke(new dgtCtrl(DgtToggleEnabled), btLogin);
|
|
return;
|
|
}
|
|
//#endif
|
|
btLogin.IsEnabled = false;
|
|
btLogin.Visibility = Visibility.Hidden;
|
|
imLogin.IsEnabled = false;
|
|
imLogin.Visibility = Visibility.Hidden;
|
|
btDislike.IsEnabled = true;
|
|
btLike.IsEnabled = true;
|
|
SetStatus($"Logged in as {await NwP.GetUser()}");
|
|
}
|
|
private async void BtLike_Click(object sender, RoutedEventArgs e) {
|
|
_ = Dispatcher.BeginInvoke(new dgtCtrl(DgtToggleEnabled), btLike);
|
|
Vote v = await NwP.Vote() ?? Vote.Neutral;
|
|
v = v switch {
|
|
Vote.Like => Vote.Favourite,
|
|
Vote.Favourite => Vote.Neutral,
|
|
_ => Vote.Like,
|
|
};
|
|
if(await NwP.Vote(v)) {
|
|
SetStatus($"Submitted {v}!");
|
|
} else
|
|
SetStatus($"Something went weird while casting {v}");
|
|
_ = Dispatcher.BeginInvoke(new dgtCtrl(DgtToggleEnabled), btLike);
|
|
_ = Dispatcher.BeginInvoke(new dgtBool(DgtRefresh), true);
|
|
}
|
|
private async void BtDislike_Click(object sender, RoutedEventArgs e) {
|
|
_ = Dispatcher.BeginInvoke(new dgtCtrl(DgtToggleEnabled), btDislike);
|
|
Vote v = Vote.Dislike;
|
|
if(await NwP.Vote() == Vote.Dislike)
|
|
v = Vote.Neutral;
|
|
if(await NwP.Vote(v))
|
|
SetStatus($"Submitted {v}!");
|
|
else
|
|
SetStatus($"Something went weird while casting {v}");
|
|
_ = Dispatcher.BeginInvoke(new dgtCtrl(DgtToggleEnabled), btDislike);
|
|
_ = Dispatcher.BeginInvoke(new dgtBool(DgtRefresh), true);
|
|
}
|
|
private void SbOnTop_Click(object sender, RoutedEventArgs e) {
|
|
Topmost = sbOnTop.IsChecked == true;
|
|
if(tGfx.Enabled && Topmost)
|
|
tGfx.Interval = 1000.0 / 60.0;
|
|
}
|
|
private void TbPlayPause_Click(object sender, EventArgs e) =>
|
|
btPlayPause.IsChecked = !btPlayPause.IsChecked;
|
|
private void TbMute_Click(object sender, EventArgs e) =>
|
|
sbMute.IsChecked = !sbMute.IsChecked;
|
|
private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) {
|
|
_ = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(e.Uri.AbsoluteUri));
|
|
e.Handled = true;
|
|
}
|
|
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) {
|
|
tRefresh.Stop();
|
|
if(AudioInit) {
|
|
tAudio.Stop();
|
|
tGfx.Stop();
|
|
PlazaStop();
|
|
}
|
|
}
|
|
private void Window_Deactivated(object sender, EventArgs e) {
|
|
if(tGfx.Enabled && !Topmost)
|
|
tGfx.Interval = 1000.0 / 25.0;
|
|
}
|
|
private void Window_Activated(object sender, EventArgs e) {
|
|
if(tGfx.Enabled)
|
|
tGfx.Interval = 1000.0 / 60.0;
|
|
}
|
|
#endregion
|
|
#region GUIMethods
|
|
public MainWindow() {
|
|
InitializeComponent();
|
|
// TODO work out if this can be made less gross
|
|
// BASS library must explicitly be named "bass.dll" in order to be loaded (seemingly..) so arch-dependent directory is needed.
|
|
if(Environment.Is64BitProcess) {
|
|
if(LoadLibrary("lib64/bass.dll") == IntPtr.Zero) {
|
|
int lasterror = Marshal.GetLastWin32Error();
|
|
System.ComponentModel.Win32Exception innerEx = new System.ComponentModel.Win32Exception(lasterror);
|
|
innerEx.Data.Add("LastWin32Error", lasterror);
|
|
throw new Exception("can't load DLL", innerEx);
|
|
}
|
|
} else {
|
|
if(LoadLibrary("lib32/bass.dll") == IntPtr.Zero) {
|
|
int lasterror = Marshal.GetLastWin32Error();
|
|
System.ComponentModel.Win32Exception innerEx = new System.ComponentModel.Win32Exception(lasterror);
|
|
innerEx.Data.Add("LastWin32Error", lasterror);
|
|
throw new Exception("can't load DLL", innerEx);
|
|
}
|
|
}
|
|
if(!Bass.Init())
|
|
sbStatus.Text = "Unable to initialise audio subsystem";
|
|
else
|
|
AudioInit = true;
|
|
tRefresh = new Timer(300) { AutoReset = true };
|
|
tRefresh.Elapsed += EvtRefresh;
|
|
tRefresh.Start();
|
|
tLogon = new Timer(1000) { AutoReset = false };
|
|
tLogon.Elapsed += EvtLogon;
|
|
tLogon.Start();
|
|
if(!AudioInit)
|
|
return;
|
|
tAudio = new Timer(50);
|
|
tAudio.Elapsed += EvtAudio;
|
|
tGfx = new Timer(16) { AutoReset = true };
|
|
tGfx.Elapsed += EvtRender;
|
|
Bass.NetPlaylist = 1;
|
|
Bass.NetPreBuffer = 0;
|
|
}
|
|
~MainWindow() {
|
|
NwP.Dispose();
|
|
if(AudioInit)
|
|
_ = Bass.Free();
|
|
}
|
|
#endregion
|
|
}
|
|
}
|