Player additions and cleanup

- Add comments and rearrange code
- Add helper methods to make some of the code easier to read and navigate
- Fix code inconsistencies that lead to bad user experiences
- Add TOC generation (setting, GD-ROM support)
This commit is contained in:
Matt Nadareski
2021-06-06 21:43:47 -07:00
parent edae3a7a58
commit 384716090d
2 changed files with 847 additions and 469 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,6 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Timers;
using Aaru.CommonTypes.Interfaces;
using Aaru.DiscImages;
using Aaru.Filters;
using Avalonia;
@@ -14,62 +13,269 @@ using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
using ReactiveUI;
namespace RedBookPlayer
{
public class PlayerView : UserControl
{
/// <summary>
/// Player representing the internal state and loaded image
/// </summary>
public static Player Player = new Player();
TextBlock currentTrack;
Image[] digits;
Timer updateTimer;
/// <summary>
/// Set of images representing the digits for the UI
/// </summary>
/// <remarks>
/// TODO: Does it make sense to have this as an array?
/// </remarks>
private Image[] _digits;
/// <summary>
/// Timer for performing UI updates
/// </summary>
private Timer _updateTimer;
public PlayerView() => InitializeComponent(null);
public PlayerView(string xaml) => InitializeComponent(xaml);
public async void LoadButton_Click(object sender, RoutedEventArgs e)
{
string path = await GetPath();
if(path == null)
{
return;
}
await Task.Run(() =>
{
var image = new AaruFormat();
IFilter filter = new ZZZNoFilter();
filter.Open(path);
image.Open(filter);
Player.Init(image, App.Settings.AutoPlay);
});
await Dispatcher.UIThread.InvokeAsync(() =>
{
MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
});
}
#region Helpers
/// <summary>
/// Generate a path selection dialog box
/// </summary>
/// <returns>User-selected path, if possible</returns>
public async Task<string> GetPath()
{
var dialog = new OpenFileDialog();
dialog.AllowMultiple = false;
var dialog = new OpenFileDialog { AllowMultiple = false };
List<string> knownExtensions = new AaruFormat().KnownExtensions.ToList();
dialog.Filters.Add(new FileDialogFilter
dialog.Filters.Add(new FileDialogFilter()
{
Name = "Aaru Image Format (*" + string.Join(", *", knownExtensions) + ")",
Extensions = knownExtensions.ConvertAll(e => e.Substring(1))
Extensions = knownExtensions.ConvertAll(e => e.TrimStart('.'))
});
return (await dialog.ShowAsync((Window)Parent.Parent))?.FirstOrDefault();
}
/// <summary>
/// Generate the digit string to be interpreted by the UI
/// </summary>
/// <returns>String representing the digits for the player</returns>
private string GenerateDigitString()
{
// If the player isn't initialized, return all '-' characters
if (!Player.Initialized)
return string.Empty.PadLeft(20, '-');
// Otherwise, take the current time into account
ulong sectorTime = Player.CurrentSector;
if (Player.SectionStartSector != 0)
sectorTime -= Player.SectionStartSector;
else
sectorTime += Player.TimeOffset;
int[] numbers = new int[]
{
Player.CurrentTrack + 1,
Player.CurrentIndex,
(int)(sectorTime / (75 * 60)),
(int)(sectorTime / 75 % 60),
(int)(sectorTime % 75),
Player.TotalTracks,
Player.TotalIndexes,
(int)(Player.TotalTime / (75 * 60)),
(int)(Player.TotalTime / 75 % 60),
(int)(Player.TotalTime % 75),
};
return string.Join("", numbers.Select(i => i.ToString().PadLeft(2, '0').Substring(0, 2)));
}
/// <summary>
/// Load the png image for a given character based on the theme
/// </summary>
/// <param name="character">Character to load the image for</param>
/// <returns>Bitmap representing the loaded image</returns>
/// <remarks>
/// TODO: Currently assumes that an image must always exist
/// </remarks>
private Bitmap GetBitmap(char character)
{
if(App.Settings.SelectedTheme == "default")
{
IAssetLoader assets = AvaloniaLocator.Current.GetService<IAssetLoader>();
return new Bitmap(assets.Open(new Uri($"avares://RedBookPlayer/Assets/{character}.png")));
}
else
{
string themeDirectory = $"{Directory.GetCurrentDirectory()}/themes/{App.Settings.SelectedTheme}";
using FileStream stream = File.Open($"{themeDirectory}/{character}.png", FileMode.Open);
return new Bitmap(stream);
}
}
/// <summary>
/// Initialize the displayed digits array
/// </summary>
private void Initialize()
{
_digits = new Image[]
{
this.FindControl<Image>("TrackDigit1"),
this.FindControl<Image>("TrackDigit2"),
this.FindControl<Image>("IndexDigit1"),
this.FindControl<Image>("IndexDigit2"),
this.FindControl<Image>("TimeDigit1"),
this.FindControl<Image>("TimeDigit2"),
this.FindControl<Image>("TimeDigit3"),
this.FindControl<Image>("TimeDigit4"),
this.FindControl<Image>("TimeDigit5"),
this.FindControl<Image>("TimeDigit6"),
this.FindControl<Image>("TotalTracksDigit1"),
this.FindControl<Image>("TotalTracksDigit2"),
this.FindControl<Image>("TotalIndexesDigit1"),
this.FindControl<Image>("TotalIndexesDigit2"),
this.FindControl<Image>("TotalTimeDigit1"),
this.FindControl<Image>("TotalTimeDigit2"),
this.FindControl<Image>("TotalTimeDigit3"),
this.FindControl<Image>("TotalTimeDigit4"),
this.FindControl<Image>("TotalTimeDigit5"),
this.FindControl<Image>("TotalTimeDigit6"),
};
}
/// <summary>
/// Initialize the UI based on the currently selected theme
/// </summary>
/// <param name="xaml">XAML data representing the theme, null for default</param>
private void InitializeComponent(string xaml)
{
DataContext = new PlayerViewModel();
if (xaml != null)
new AvaloniaXamlLoader().Load(xaml, null, this);
else
AvaloniaXamlLoader.Load(this);
Initialize();
_updateTimer = new Timer(1000 / 60);
_updateTimer.Elapsed += (sender, e) =>
{
try
{
UpdateView(sender, e);
}
catch(Exception ex)
{
Console.WriteLine(ex);
}
};
_updateTimer.AutoReset = true;
_updateTimer.Start();
}
/// <summary>
/// Indicates if the image is considered "playable" or not
/// </summary>
/// <param name="image">Aaruformat image file</param>
/// <returns>True if the image is playble, false otherwise</returns>
private bool IsPlayableImage(AaruFormat image)
{
// Invalid images can't be played
if (image == null)
return false;
// Tape images are not supported
if (image.IsTape)
return false;
// Determine based on media type
// TODO: Can we be more granular with sub types?
(string type, string _) = Aaru.CommonTypes.Metadata.MediaType.MediaTypeToString(image.Info.MediaType);
return type switch
{
"Compact Disc" => true,
"GD" => true, // Requires TOC generation
_ => false,
};
}
/// <summary>
/// Update the UI with the most recent information from the Player
/// </summary>
private void UpdateView(object sender, ElapsedEventArgs e)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
string digitString = GenerateDigitString();
for (int i = 0; i < _digits.Length; i++)
{
if (_digits[i] != null)
_digits[i].Source = GetBitmap(digitString[i]);
}
if (Player.Initialized)
{
PlayerViewModel dataContext = (PlayerViewModel)DataContext;
dataContext.HiddenTrack = Player.TimeOffset > 150;
dataContext.ApplyDeEmphasis = Player.ApplyDeEmphasis;
dataContext.TrackHasEmphasis = Player.TrackHasEmphasis;
dataContext.CopyAllowed = Player.CopyAllowed;
dataContext.IsAudioTrack = Player.TrackType == TrackTypeValue.Audio;
dataContext.IsDataTrack = Player.TrackType == TrackTypeValue.Data;
}
});
}
#endregion
#region Event Handlers
public async void LoadButton_Click(object sender, RoutedEventArgs e)
{
string path = await GetPath();
if (path == null)
return;
bool result = await Task.Run(() =>
{
var image = new AaruFormat();
var filter = new ZZZNoFilter();
filter.Open(path);
image.Open(filter);
if (IsPlayableImage(image))
{
Player.Init(image, App.Settings.AutoPlay);
return true;
}
else
return false;
});
if (result)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
});
}
}
public void PlayButton_Click(object sender, RoutedEventArgs e) => Player.Play();
public void PauseButton_Click(object sender, RoutedEventArgs e) => Player.Pause();
@@ -80,208 +286,18 @@ namespace RedBookPlayer
public void PreviousTrackButton_Click(object sender, RoutedEventArgs e) => Player.PreviousTrack();
public void NextIndexButton_Click(object sender, RoutedEventArgs e) =>
Player.NextIndex(App.Settings.IndexButtonChangeTrack);
public void NextIndexButton_Click(object sender, RoutedEventArgs e) => Player.NextIndex(App.Settings.IndexButtonChangeTrack);
public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) =>
Player.PreviousIndex(App.Settings.IndexButtonChangeTrack);
public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) => Player.PreviousIndex(App.Settings.IndexButtonChangeTrack);
public void FastForwardButton_Click(object sender, RoutedEventArgs e) => Player.FastForward();
public void RewindButton_Click(object sender, RoutedEventArgs e) => Player.Rewind();
public void EnableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.EnableDeEmphasis();
public void EnableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.ToggleDeEmphasis(true);
public void DisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.DisableDeEmphasis();
public void DisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.ToggleDeEmphasis(false);
void UpdateView(object sender, ElapsedEventArgs e)
{
if(Player.Initialized)
{
ulong sectorTime = Player.CurrentSector;
if(Player.SectionStartSector != 0)
{
sectorTime -= Player.SectionStartSector;
}
else
{
sectorTime += Player.TimeOffset;
}
int[] numbers =
{
Player.CurrentTrack + 1, Player.CurrentIndex, (int)(sectorTime / (75 * 60)),
(int)(sectorTime / 75 % 60), (int)(sectorTime % 75), Player.TotalTracks, Player.TotalIndexes,
(int)(Player.TotalTime / (75 * 60)), (int)(Player.TotalTime / 75 % 60), (int)(Player.TotalTime % 75)
};
string digitString = string.Join("", numbers.Select(i => i.ToString().PadLeft(2, '0').Substring(0, 2)));
Dispatcher.UIThread.InvokeAsync(() =>
{
for(int i = 0; i < digits.Length; i++)
{
if(digits[i] != null)
{
digits[i].Source = GetBitmap(digitString[i]);
}
}
var dataContext = (PlayerViewModel)DataContext;
dataContext.HiddenTrack = Player.TimeOffset > 150;
dataContext.ApplyDeEmphasis = Player.ApplyDeEmphasis;
dataContext.TrackHasEmphasis = Player.TrackHasEmphasis;
dataContext.CopyAllowed = Player.CopyAllowed;
dataContext.IsAudioTrack = Player.TrackType_ == Player.TrackType.Audio;
dataContext.IsDataTrack = Player.TrackType_ == Player.TrackType.Data;
});
}
else
{
Dispatcher.UIThread.InvokeAsync(() =>
{
foreach(Image digit in digits)
{
if(digit != null)
{
digit.Source = GetBitmap('-');
}
}
});
}
}
Bitmap GetBitmap(char character)
{
if(App.Settings.SelectedTheme == "default")
{
IAssetLoader assets = AvaloniaLocator.Current.GetService<IAssetLoader>();
return new Bitmap(assets.Open(new Uri($"avares://RedBookPlayer/Assets/{character}.png")));
}
string themeDirectory = Directory.GetCurrentDirectory() + "/themes/" + App.Settings.SelectedTheme;
Bitmap bitmap;
using(FileStream stream = File.Open(themeDirectory + $"/{character}.png", FileMode.Open))
{
bitmap = new Bitmap(stream);
}
return bitmap;
}
public void Initialize()
{
digits = new Image[20];
digits[0] = this.FindControl<Image>("TrackDigit1");
digits[1] = this.FindControl<Image>("TrackDigit2");
digits[2] = this.FindControl<Image>("IndexDigit1");
digits[3] = this.FindControl<Image>("IndexDigit2");
digits[4] = this.FindControl<Image>("TimeDigit1");
digits[5] = this.FindControl<Image>("TimeDigit2");
digits[6] = this.FindControl<Image>("TimeDigit3");
digits[7] = this.FindControl<Image>("TimeDigit4");
digits[8] = this.FindControl<Image>("TimeDigit5");
digits[9] = this.FindControl<Image>("TimeDigit6");
digits[10] = this.FindControl<Image>("TotalTracksDigit1");
digits[11] = this.FindControl<Image>("TotalTracksDigit2");
digits[12] = this.FindControl<Image>("TotalIndexesDigit1");
digits[13] = this.FindControl<Image>("TotalIndexesDigit2");
digits[14] = this.FindControl<Image>("TotalTimeDigit1");
digits[15] = this.FindControl<Image>("TotalTimeDigit2");
digits[16] = this.FindControl<Image>("TotalTimeDigit3");
digits[17] = this.FindControl<Image>("TotalTimeDigit4");
digits[18] = this.FindControl<Image>("TotalTimeDigit5");
digits[19] = this.FindControl<Image>("TotalTimeDigit6");
currentTrack = this.FindControl<TextBlock>("CurrentTrack");
}
void InitializeComponent(string xaml)
{
DataContext = new PlayerViewModel();
if(xaml != null)
{
new AvaloniaXamlLoader().Load(xaml, null, this);
}
else
{
AvaloniaXamlLoader.Load(this);
}
Initialize();
updateTimer = new Timer(1000 / 60);
updateTimer.Elapsed += (sender, e) =>
{
try
{
UpdateView(sender, e);
}
catch(Exception ex)
{
Console.WriteLine(ex);
}
};
updateTimer.AutoReset = true;
updateTimer.Start();
}
}
public class PlayerViewModel : ReactiveObject
{
bool applyDeEmphasis;
bool copyAllowed;
bool hiddenTrack;
bool isAudioTrack;
bool isDataTrack;
bool trackHasEmphasis;
public bool ApplyDeEmphasis
{
get => applyDeEmphasis;
set => this.RaiseAndSetIfChanged(ref applyDeEmphasis, value);
}
public bool TrackHasEmphasis
{
get => trackHasEmphasis;
set => this.RaiseAndSetIfChanged(ref trackHasEmphasis, value);
}
public bool HiddenTrack
{
get => hiddenTrack;
set => this.RaiseAndSetIfChanged(ref hiddenTrack, value);
}
public bool CopyAllowed
{
get => copyAllowed;
set => this.RaiseAndSetIfChanged(ref copyAllowed, value);
}
public bool IsAudioTrack
{
get => isAudioTrack;
set => this.RaiseAndSetIfChanged(ref isAudioTrack, value);
}
public bool IsDataTrack
{
get => isDataTrack;
set => this.RaiseAndSetIfChanged(ref isDataTrack, value);
}
#endregion
}
}