Port only MVVM work and related fixes
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
* This project is fully sponsored by the [Game Preservation Society](https://www.gamepres.org/en/).
|
* This project is fully sponsored by the [Game Preservation Society](https://www.gamepres.org/en/).
|
||||||
|
|
||||||
|
[OpenAL](https://www.openal.org/) is required to run this application. Please install it using the most recent instructions for your operating system of choice.
|
||||||
|
|
||||||
## Default Player Controls
|
## Default Player Controls
|
||||||
|
|
||||||
| Key | Action |
|
| Key | Action |
|
||||||
|
|||||||
@@ -5,14 +5,20 @@ using Avalonia;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using RedBookPlayer.GUI;
|
using RedBookPlayer.GUI.ViewModels;
|
||||||
using RedBookPlayer.GUI.Views;
|
using RedBookPlayer.GUI.Views;
|
||||||
|
|
||||||
namespace RedBookPlayer
|
namespace RedBookPlayer
|
||||||
{
|
{
|
||||||
public class App : Application
|
public class App : Application
|
||||||
{
|
{
|
||||||
public static Settings Settings;
|
public static MainWindow MainWindow;
|
||||||
|
public static SettingsViewModel Settings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read-only access to the current player view
|
||||||
|
/// </summary>
|
||||||
|
public static PlayerView PlayerView => MainWindow?.ViewModel?.PlayerView;
|
||||||
|
|
||||||
static App() =>
|
static App() =>
|
||||||
Directory.SetCurrentDirectory(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName));
|
Directory.SetCurrentDirectory(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName));
|
||||||
@@ -24,6 +30,7 @@ namespace RedBookPlayer
|
|||||||
Console.WriteLine(((Exception)f.ExceptionObject).ToString());
|
Console.WriteLine(((Exception)f.ExceptionObject).ToString());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Settings = SettingsViewModel.Load("settings.json");
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,10 +38,11 @@ namespace RedBookPlayer
|
|||||||
{
|
{
|
||||||
if(ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if(ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
desktop.MainWindow = new MainWindow();
|
MainWindow = new MainWindow();
|
||||||
|
desktop.MainWindow = MainWindow;
|
||||||
desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
|
desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
|
||||||
|
|
||||||
Settings = Settings.Load("settings.json");
|
PlayerView.ViewModel.ApplyTheme(Settings.SelectedTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
|
|||||||
#endif
|
#endif
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Logging.Serilog;
|
using Avalonia.Logging.Serilog;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
namespace RedBookPlayer.GUI
|
namespace RedBookPlayer.GUI
|
||||||
{
|
{
|
||||||
@@ -24,6 +25,6 @@ namespace RedBookPlayer.GUI
|
|||||||
static extern bool AllocConsole();
|
static extern bool AllocConsole();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>().UsePlatformDetect().LogToDebug();
|
public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>().UseReactiveUI().UsePlatformDetect().LogToDebug();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<Compile Update="**\*.xaml.cs">
|
<Compile Update="**\*.xaml.cs">
|
||||||
<DependentUpon>%(Filename)</DependentUpon>
|
<DependentUpon>%(Filename)</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<AvaloniaResource Include="**\*.xaml" Exclude="bin\**;obj\**">
|
<AvaloniaResource Include="**\*.xaml" Exclude="bin\**;obj\**;themes\**">
|
||||||
<SubType>Designer</SubType>
|
<SubType>Designer</SubType>
|
||||||
</AvaloniaResource>
|
</AvaloniaResource>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -28,4 +28,11 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="Assets\*" />
|
<AvaloniaResource Include="Assets\*" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="themes\**">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
|
||||||
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
159
RedBookPlayer.GUI/ViewModels/MainViewModel.cs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using RedBookPlayer.GUI.Views;
|
||||||
|
|
||||||
|
namespace RedBookPlayer.GUI.ViewModels
|
||||||
|
{
|
||||||
|
public class MainViewModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Read-only access to the control
|
||||||
|
/// </summary>
|
||||||
|
public ContentControl ContentControl => App.MainWindow.FindControl<ContentControl>("Content");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read-only access to the view
|
||||||
|
/// </summary>
|
||||||
|
public PlayerView PlayerView => ContentControl?.Content as PlayerView;
|
||||||
|
|
||||||
|
#region Helpers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute the result of a keypress
|
||||||
|
/// </summary>
|
||||||
|
public async void ExecuteKeyPress(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
// Open settings window
|
||||||
|
if(e.Key == App.Settings.OpenSettingsKey)
|
||||||
|
{
|
||||||
|
SettingsWindow settingsWindow = new SettingsWindow() { DataContext = App.Settings };
|
||||||
|
settingsWindow.Closed += OnSettingsClosed;
|
||||||
|
settingsWindow.ShowDialog(App.MainWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load image
|
||||||
|
else if(e.Key == App.Settings.LoadImageKey)
|
||||||
|
{
|
||||||
|
PlayerView?.ViewModel?.ExecuteLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle playback
|
||||||
|
else if(e.Key == App.Settings.TogglePlaybackKey || e.Key == Key.MediaPlayPause)
|
||||||
|
{
|
||||||
|
PlayerView?.ViewModel?.ExecuteTogglePlayPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop playback
|
||||||
|
else if(e.Key == App.Settings.StopPlaybackKey || e.Key == Key.MediaStop)
|
||||||
|
{
|
||||||
|
PlayerView?.ViewModel?.ExecuteStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next Track
|
||||||
|
else if(e.Key == App.Settings.NextTrackKey || e.Key == Key.MediaNextTrack)
|
||||||
|
{
|
||||||
|
PlayerView?.ViewModel?.ExecuteNextTrack();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Previous Track
|
||||||
|
else if(e.Key == App.Settings.PreviousTrackKey || e.Key == Key.MediaPreviousTrack)
|
||||||
|
{
|
||||||
|
PlayerView?.ViewModel?.ExecutePreviousTrack();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next Index
|
||||||
|
else if(e.Key == App.Settings.NextIndexKey)
|
||||||
|
{
|
||||||
|
PlayerView?.ViewModel?.ExecuteNextIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Previous Index
|
||||||
|
else if(e.Key == App.Settings.PreviousIndexKey)
|
||||||
|
{
|
||||||
|
PlayerView?.ViewModel?.ExecutePreviousIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast Foward
|
||||||
|
else if(e.Key == App.Settings.FastForwardPlaybackKey)
|
||||||
|
{
|
||||||
|
PlayerView?.ViewModel?.ExecuteFastForward();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewind
|
||||||
|
else if(e.Key == App.Settings.RewindPlaybackKey)
|
||||||
|
{
|
||||||
|
PlayerView?.ViewModel?.ExecuteRewind();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volume Up
|
||||||
|
else if(e.Key == App.Settings.VolumeUpKey || e.Key == Key.VolumeUp)
|
||||||
|
{
|
||||||
|
int increment = 1;
|
||||||
|
if(e.KeyModifiers.HasFlag(KeyModifiers.Control))
|
||||||
|
increment *= 2;
|
||||||
|
if(e.KeyModifiers.HasFlag(KeyModifiers.Shift))
|
||||||
|
increment *= 5;
|
||||||
|
|
||||||
|
if(PlayerView?.ViewModel?.Volume != null)
|
||||||
|
PlayerView.ViewModel.ExecuteSetVolume(PlayerView.ViewModel.Volume + increment);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volume Down
|
||||||
|
else if(e.Key == App.Settings.VolumeDownKey || e.Key == Key.VolumeDown)
|
||||||
|
{
|
||||||
|
int decrement = 1;
|
||||||
|
if(e.KeyModifiers.HasFlag(KeyModifiers.Control))
|
||||||
|
decrement *= 2;
|
||||||
|
if(e.KeyModifiers.HasFlag(KeyModifiers.Shift))
|
||||||
|
decrement *= 5;
|
||||||
|
|
||||||
|
if(PlayerView?.ViewModel?.Volume != null)
|
||||||
|
PlayerView.ViewModel.ExecuteSetVolume(PlayerView.ViewModel.Volume - decrement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mute Toggle
|
||||||
|
else if(e.Key == App.Settings.ToggleMuteKey || e.Key == Key.VolumeMute)
|
||||||
|
{
|
||||||
|
PlayerView?.ViewModel?.ExecuteToggleMute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emphasis Toggle
|
||||||
|
else if(e.Key == App.Settings.ToggleDeEmphasisKey)
|
||||||
|
{
|
||||||
|
PlayerView?.ViewModel?.ExecuteToggleDeEmphasis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load the first valid drag-and-dropped disc image
|
||||||
|
/// </summary>
|
||||||
|
public async void ExecuteLoadDragDrop(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if(PlayerView?.ViewModel == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
IEnumerable<string> fileNames = e.Data.GetFileNames();
|
||||||
|
foreach(string filename in fileNames)
|
||||||
|
{
|
||||||
|
bool loaded = await PlayerView.ViewModel.LoadImage(filename);
|
||||||
|
if(loaded)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop current playback
|
||||||
|
/// </summary>
|
||||||
|
public void ExecuteStop(object sender, CancelEventArgs e) => PlayerView?.ViewModel?.ExecuteStop();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle the settings window closing
|
||||||
|
/// </summary>
|
||||||
|
private void OnSettingsClosed(object sender, EventArgs e) => PlayerView?.ViewModel?.RefreshFromSettings();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,13 +5,16 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using RedBookPlayer.GUI.Views;
|
using RedBookPlayer.Models;
|
||||||
|
using RedBookPlayer.Models.Discs;
|
||||||
using RedBookPlayer.Models.Hardware;
|
using RedBookPlayer.Models.Hardware;
|
||||||
|
|
||||||
namespace RedBookPlayer.GUI.ViewModels
|
namespace RedBookPlayer.GUI.ViewModels
|
||||||
@@ -21,7 +24,7 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Player representing the internal state
|
/// Player representing the internal state
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private Player _player;
|
private readonly Player _player;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set of images representing the digits for the UI
|
/// Set of images representing the digits for the UI
|
||||||
@@ -50,6 +53,15 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
private set => this.RaiseAndSetIfChanged(ref _currentTrackIndex, value);
|
private set => this.RaiseAndSetIfChanged(ref _currentTrackIndex, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current track session
|
||||||
|
/// </summary>
|
||||||
|
public ushort CurrentTrackSession
|
||||||
|
{
|
||||||
|
get => _currentTrackSession;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _currentTrackSession, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Current sector number
|
/// Current sector number
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -140,6 +152,7 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
|
|
||||||
private int _currentTrackNumber;
|
private int _currentTrackNumber;
|
||||||
private ushort _currentTrackIndex;
|
private ushort _currentTrackIndex;
|
||||||
|
private ushort _currentTrackSession;
|
||||||
private ulong _currentSector;
|
private ulong _currentSector;
|
||||||
private ulong _sectionStartSector;
|
private ulong _sectionStartSector;
|
||||||
|
|
||||||
@@ -156,15 +169,28 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicate if the model is ready to be used
|
/// Indicate if the model is ready to be used
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Initialized => _player?.Initialized ?? false;
|
public bool Initialized
|
||||||
|
{
|
||||||
|
get => _initialized;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _initialized, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicate if the output is playing
|
/// Indicate if the output is playing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? Playing
|
public PlayerState PlayerState
|
||||||
{
|
{
|
||||||
get => _playing;
|
get => _playerState;
|
||||||
private set => this.RaiseAndSetIfChanged(ref _playing, value);
|
private set => this.RaiseAndSetIfChanged(ref _playerState, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates how to handle playback of data tracks
|
||||||
|
/// </summary>
|
||||||
|
public DataPlayback DataPlayback
|
||||||
|
{
|
||||||
|
get => _dataPlayback;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _dataPlayback, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -185,7 +211,9 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
private set => this.RaiseAndSetIfChanged(ref _volume, value);
|
private set => this.RaiseAndSetIfChanged(ref _volume, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool? _playing;
|
private bool _initialized;
|
||||||
|
private PlayerState _playerState;
|
||||||
|
private DataPlayback _dataPlayback;
|
||||||
private bool _applyDeEmphasis;
|
private bool _applyDeEmphasis;
|
||||||
private int _volume;
|
private int _volume;
|
||||||
|
|
||||||
@@ -299,6 +327,7 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public PlayerViewModel()
|
public PlayerViewModel()
|
||||||
{
|
{
|
||||||
|
// Initialize commands
|
||||||
LoadCommand = ReactiveCommand.Create(ExecuteLoad);
|
LoadCommand = ReactiveCommand.Create(ExecuteLoad);
|
||||||
|
|
||||||
PlayCommand = ReactiveCommand.Create(ExecutePlay);
|
PlayCommand = ReactiveCommand.Create(ExecutePlay);
|
||||||
@@ -319,25 +348,27 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
EnableDeEmphasisCommand = ReactiveCommand.Create(ExecuteEnableDeEmphasis);
|
EnableDeEmphasisCommand = ReactiveCommand.Create(ExecuteEnableDeEmphasis);
|
||||||
DisableDeEmphasisCommand = ReactiveCommand.Create(ExecuteDisableDeEmphasis);
|
DisableDeEmphasisCommand = ReactiveCommand.Create(ExecuteDisableDeEmphasis);
|
||||||
ToggleDeEmphasisCommand = ReactiveCommand.Create(ExecuteToggleDeEmphasis);
|
ToggleDeEmphasisCommand = ReactiveCommand.Create(ExecuteToggleDeEmphasis);
|
||||||
|
|
||||||
|
// Initialize Player
|
||||||
|
_player = new Player(App.Settings.Volume);
|
||||||
|
PlayerState = PlayerState.NoDisc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialize the view model with a given image path
|
/// Initialize the view model with a given image path
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Path to the disc image</param>
|
/// <param name="path">Path to the disc image</param>
|
||||||
/// <param name="generateMissingToc">Generate a TOC if the disc is missing one [CompactDisc only]</param>
|
/// <param name="options">Options to pass to the optical disc factory</param>
|
||||||
/// <param name="loadHiddenTracks">Load hidden tracks for playback [CompactDisc only]</param>
|
|
||||||
/// <param name="loadDataTracks">Load data tracks for playback [CompactDisc only]</param>
|
|
||||||
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
||||||
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param>
|
public void Init(string path, OpticalDiscOptions options, bool autoPlay)
|
||||||
public void Init(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay, int defaultVolume)
|
|
||||||
{
|
{
|
||||||
// Stop current playback, if necessary
|
// Stop current playback, if necessary
|
||||||
if(Playing != null) ExecuteStop();
|
if(PlayerState != PlayerState.NoDisc)
|
||||||
|
ExecuteStop();
|
||||||
|
|
||||||
// Create and attempt to initialize new Player
|
// Attempt to initialize Player
|
||||||
_player = new Player(path, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay, defaultVolume);
|
_player.Init(path, options, autoPlay);
|
||||||
if(Initialized)
|
if(_player.Initialized)
|
||||||
{
|
{
|
||||||
_player.PropertyChanged += PlayerStateChanged;
|
_player.PropertyChanged += PlayerStateChanged;
|
||||||
PlayerStateChanged(this, null);
|
PlayerStateChanged(this, null);
|
||||||
@@ -444,6 +475,46 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
|
|
||||||
#region Helpers
|
#region Helpers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apply a custom theme to the player
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="theme">Path to the theme under the themes directory</param>
|
||||||
|
public void ApplyTheme(string theme)
|
||||||
|
{
|
||||||
|
// If the PlayerView isn't set, don't do anything
|
||||||
|
if(App.PlayerView == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// If no theme path is provided, we can ignore
|
||||||
|
if(string.IsNullOrWhiteSpace(theme))
|
||||||
|
return;
|
||||||
|
|
||||||
|
string themeDirectory = $"{Directory.GetCurrentDirectory()}/themes/{theme}";
|
||||||
|
string xamlPath = $"{themeDirectory}/view.xaml";
|
||||||
|
|
||||||
|
if(!File.Exists(xamlPath))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Warning: specified theme doesn't exist, reverting to default");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string xaml = File.ReadAllText(xamlPath);
|
||||||
|
xaml = xaml.Replace("Source=\"", $"Source=\"file://{themeDirectory}/");
|
||||||
|
LoadTheme(xaml);
|
||||||
|
}
|
||||||
|
catch(XmlException ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error: invalid theme XAML ({ex.Message}), reverting to default");
|
||||||
|
LoadTheme(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
App.MainWindow.Width = App.PlayerView.Width;
|
||||||
|
App.MainWindow.Height = App.PlayerView.Height;
|
||||||
|
InitializeDigits();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load a disc image from a selection box
|
/// Load a disc image from a selection box
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -461,35 +532,36 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void InitializeDigits()
|
public void InitializeDigits()
|
||||||
{
|
{
|
||||||
PlayerView playerView = MainWindow.Instance.ContentControl.Content as PlayerView;
|
if(App.PlayerView == null)
|
||||||
|
return;
|
||||||
|
|
||||||
_digits = new Image[]
|
_digits = new Image[]
|
||||||
{
|
{
|
||||||
playerView.FindControl<Image>("TrackDigit1"),
|
App.PlayerView.FindControl<Image>("TrackDigit1"),
|
||||||
playerView.FindControl<Image>("TrackDigit2"),
|
App.PlayerView.FindControl<Image>("TrackDigit2"),
|
||||||
|
|
||||||
playerView.FindControl<Image>("IndexDigit1"),
|
App.PlayerView.FindControl<Image>("IndexDigit1"),
|
||||||
playerView.FindControl<Image>("IndexDigit2"),
|
App.PlayerView.FindControl<Image>("IndexDigit2"),
|
||||||
|
|
||||||
playerView.FindControl<Image>("TimeDigit1"),
|
App.PlayerView.FindControl<Image>("TimeDigit1"),
|
||||||
playerView.FindControl<Image>("TimeDigit2"),
|
App.PlayerView.FindControl<Image>("TimeDigit2"),
|
||||||
playerView.FindControl<Image>("TimeDigit3"),
|
App.PlayerView.FindControl<Image>("TimeDigit3"),
|
||||||
playerView.FindControl<Image>("TimeDigit4"),
|
App.PlayerView.FindControl<Image>("TimeDigit4"),
|
||||||
playerView.FindControl<Image>("TimeDigit5"),
|
App.PlayerView.FindControl<Image>("TimeDigit5"),
|
||||||
playerView.FindControl<Image>("TimeDigit6"),
|
App.PlayerView.FindControl<Image>("TimeDigit6"),
|
||||||
|
|
||||||
playerView.FindControl<Image>("TotalTracksDigit1"),
|
App.PlayerView.FindControl<Image>("TotalTracksDigit1"),
|
||||||
playerView.FindControl<Image>("TotalTracksDigit2"),
|
App.PlayerView.FindControl<Image>("TotalTracksDigit2"),
|
||||||
|
|
||||||
playerView.FindControl<Image>("TotalIndexesDigit1"),
|
App.PlayerView.FindControl<Image>("TotalIndexesDigit1"),
|
||||||
playerView.FindControl<Image>("TotalIndexesDigit2"),
|
App.PlayerView.FindControl<Image>("TotalIndexesDigit2"),
|
||||||
|
|
||||||
playerView.FindControl<Image>("TotalTimeDigit1"),
|
App.PlayerView.FindControl<Image>("TotalTimeDigit1"),
|
||||||
playerView.FindControl<Image>("TotalTimeDigit2"),
|
App.PlayerView.FindControl<Image>("TotalTimeDigit2"),
|
||||||
playerView.FindControl<Image>("TotalTimeDigit3"),
|
App.PlayerView.FindControl<Image>("TotalTimeDigit3"),
|
||||||
playerView.FindControl<Image>("TotalTimeDigit4"),
|
App.PlayerView.FindControl<Image>("TotalTimeDigit4"),
|
||||||
playerView.FindControl<Image>("TotalTimeDigit5"),
|
App.PlayerView.FindControl<Image>("TotalTimeDigit5"),
|
||||||
playerView.FindControl<Image>("TotalTimeDigit6"),
|
App.PlayerView.FindControl<Image>("TotalTimeDigit6"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,19 +573,39 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
{
|
{
|
||||||
return await Dispatcher.UIThread.InvokeAsync(() =>
|
return await Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
Init(path, App.Settings.GenerateMissingTOC, App.Settings.PlayHiddenTracks, App.Settings.PlayDataTracks, App.Settings.AutoPlay, App.Settings.Volume);
|
OpticalDiscOptions options = new OpticalDiscOptions
|
||||||
|
{
|
||||||
|
DataPlayback = App.Settings.DataPlayback,
|
||||||
|
GenerateMissingToc = App.Settings.GenerateMissingTOC,
|
||||||
|
LoadHiddenTracks = App.Settings.PlayHiddenTracks,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure the context and view model are set
|
||||||
|
App.PlayerView.DataContext = this;
|
||||||
|
App.PlayerView.ViewModel = this;
|
||||||
|
|
||||||
|
Init(path, options, App.Settings.AutoPlay);
|
||||||
if(Initialized)
|
if(Initialized)
|
||||||
MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
|
App.MainWindow.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
|
||||||
|
|
||||||
return Initialized;
|
return Initialized;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the value for loading data tracks [CompactDisc only]
|
/// Refresh the view model from the current settings
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="load">True to enable loading data tracks, false otherwise</param>
|
public void RefreshFromSettings()
|
||||||
public void SetLoadDataTracks(bool load) => _player?.SetLoadDataTracks(load);
|
{
|
||||||
|
SetDataPlayback(App.Settings.DataPlayback);
|
||||||
|
SetLoadHiddenTracks(App.Settings.PlayHiddenTracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set data playback method [CompactDisc only]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dataPlayback">New playback value</param>
|
||||||
|
public void SetDataPlayback(DataPlayback dataPlayback) => _player?.SetDataPlayback(dataPlayback);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the value for loading hidden tracks [CompactDisc only]
|
/// Set the value for loading hidden tracks [CompactDisc only]
|
||||||
@@ -569,18 +661,9 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if(App.Settings.SelectedTheme == "default")
|
string themeDirectory = $"{Directory.GetCurrentDirectory()}/themes/{App.Settings.SelectedTheme}";
|
||||||
{
|
using FileStream stream = File.Open($"{themeDirectory}/{character}.png", FileMode.Open);
|
||||||
IAssetLoader assets = AvaloniaLocator.Current.GetService<IAssetLoader>();
|
return new Bitmap(stream);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -619,18 +702,56 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
Extensions = knownExtensions.ConvertAll(e => e.TrimStart('.'))
|
Extensions = knownExtensions.ConvertAll(e => e.TrimStart('.'))
|
||||||
});
|
});
|
||||||
|
|
||||||
return (await dialog.ShowAsync(MainWindow.Instance))?.FirstOrDefault();
|
return (await dialog.ShowAsync(App.MainWindow))?.FirstOrDefault();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load the theme from a XAML, if possible
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="xaml">XAML data representing the theme, null for default</param>
|
||||||
|
private void LoadTheme(string xaml)
|
||||||
|
{
|
||||||
|
// If the view is null, we can't load the theme
|
||||||
|
if(App.PlayerView == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if(xaml != null)
|
||||||
|
new AvaloniaXamlLoader().Load(xaml, null, App.PlayerView);
|
||||||
|
else
|
||||||
|
AvaloniaXamlLoader.Load(App.PlayerView);
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the context and view model are set
|
||||||
|
App.PlayerView.DataContext = this;
|
||||||
|
App.PlayerView.ViewModel = this;
|
||||||
|
UpdateDigits();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update the view-model from the Player
|
/// Update the view-model from the Player
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void PlayerStateChanged(object sender, PropertyChangedEventArgs e)
|
private void PlayerStateChanged(object sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if(_player?.Initialized != true)
|
if(_player == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if(!_player.Initialized)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
App.MainWindow.Title = "RedBookPlayer";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Initialized = _player.Initialized;
|
||||||
|
|
||||||
CurrentTrackNumber = _player.CurrentTrackNumber;
|
CurrentTrackNumber = _player.CurrentTrackNumber;
|
||||||
CurrentTrackIndex = _player.CurrentTrackIndex;
|
CurrentTrackIndex = _player.CurrentTrackIndex;
|
||||||
CurrentSector = _player.CurrentSector;
|
CurrentSector = _player.CurrentSector;
|
||||||
@@ -643,7 +764,8 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
CopyAllowed = _player.CopyAllowed;
|
CopyAllowed = _player.CopyAllowed;
|
||||||
TrackHasEmphasis = _player.TrackHasEmphasis;
|
TrackHasEmphasis = _player.TrackHasEmphasis;
|
||||||
|
|
||||||
Playing = _player.Playing;
|
PlayerState = _player.PlayerState;
|
||||||
|
DataPlayback = _player.DataPlayback;
|
||||||
ApplyDeEmphasis = _player.ApplyDeEmphasis;
|
ApplyDeEmphasis = _player.ApplyDeEmphasis;
|
||||||
Volume = _player.Volume;
|
Volume = _player.Volume;
|
||||||
|
|
||||||
@@ -655,6 +777,10 @@ namespace RedBookPlayer.GUI.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void UpdateDigits()
|
private void UpdateDigits()
|
||||||
{
|
{
|
||||||
|
// Ensure the digits
|
||||||
|
if(_digits == null)
|
||||||
|
InitializeDigits();
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
string digitString = GenerateDigitString() ?? string.Empty.PadLeft(20, '-');
|
string digitString = GenerateDigitString() ?? string.Empty.PadLeft(20, '-');
|
||||||
|
|||||||
@@ -1,15 +1,32 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using RedBookPlayer.GUI.Views;
|
using ReactiveUI;
|
||||||
|
using RedBookPlayer.Models;
|
||||||
|
|
||||||
namespace RedBookPlayer.GUI
|
namespace RedBookPlayer.GUI.ViewModels
|
||||||
{
|
{
|
||||||
public class Settings
|
public class SettingsViewModel : ReactiveObject
|
||||||
{
|
{
|
||||||
#region Player Settings
|
#region Player Settings
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of all data playback values
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public List<DataPlayback> DataPlaybackValues => GenerateDataPlaybackList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of all themes
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public List<string> ThemeValues => GenerateThemeList();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates if discs should start playing on load
|
/// Indicates if discs should start playing on load
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -30,42 +47,51 @@ namespace RedBookPlayer.GUI
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public bool PlayHiddenTracks { get; set; } = false;
|
public bool PlayHiddenTracks { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates if data tracks should be played like old, non-compliant players
|
|
||||||
/// </summary>
|
|
||||||
public bool PlayDataTracks { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generate a TOC if the disc is missing one
|
/// Generate a TOC if the disc is missing one
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool GenerateMissingTOC { get; set; } = true;
|
public bool GenerateMissingTOC { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates how to deal with data tracks
|
||||||
|
/// </summary>
|
||||||
|
public DataPlayback DataPlayback { get; set; } = DataPlayback.Skip;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates the default playback volume
|
/// Indicates the default playback volume
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Volume
|
public int Volume
|
||||||
{
|
{
|
||||||
get => _volume;
|
get => _volume;
|
||||||
set
|
private set
|
||||||
{
|
{
|
||||||
|
int tempValue;
|
||||||
if(value > 100)
|
if(value > 100)
|
||||||
_volume = 100;
|
tempValue = 100;
|
||||||
else if(value < 0)
|
else if(value < 0)
|
||||||
_volume = 0;
|
tempValue = 0;
|
||||||
else
|
else
|
||||||
_volume = value;
|
tempValue = value;
|
||||||
|
|
||||||
|
this.RaiseAndSetIfChanged(ref _volume, tempValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates the currently selected theme
|
/// Indicates the currently selected theme
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string SelectedTheme { get; set; } = "default";
|
public string SelectedTheme { get; set; } = "Default";
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Key Mappings
|
#region Key Mappings
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of all keyboard keys
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public List<Key> KeyboardList => GenerateKeyList();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Key assigned to open settings
|
/// Key assigned to open settings
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -86,6 +112,11 @@ namespace RedBookPlayer.GUI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Key StopPlaybackKey { get; set; } = Key.Escape;
|
public Key StopPlaybackKey { get; set; } = Key.Escape;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Key assigned to eject the disc
|
||||||
|
/// </summary>
|
||||||
|
public Key EjectKey { get; set; } = Key.OemTilde;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Key assigned to move to the next track
|
/// Key assigned to move to the next track
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -138,6 +169,15 @@ namespace RedBookPlayer.GUI
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Commands
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for applying settings
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> ApplySettingsCommand { get; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Path to the settings file
|
/// Path to the settings file
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -148,44 +188,50 @@ namespace RedBookPlayer.GUI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private int _volume = 100;
|
private int _volume = 100;
|
||||||
|
|
||||||
public Settings() {}
|
public SettingsViewModel() : this(null) { }
|
||||||
|
|
||||||
public Settings(string filePath) => _filePath = filePath;
|
public SettingsViewModel(string filePath)
|
||||||
|
{
|
||||||
|
_filePath = filePath;
|
||||||
|
|
||||||
|
ApplySettingsCommand = ReactiveCommand.Create(ExecuteApplySettings);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load settings from a file
|
/// Load settings from a file
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="filePath">Path to the settings JSON file</param>
|
/// <param name="filePath">Path to the settings JSON file</param>
|
||||||
/// <returns>Settings derived from the input file, if possible</returns>
|
/// <returns>Settings derived from the input file, if possible</returns>
|
||||||
public static Settings Load(string filePath)
|
public static SettingsViewModel Load(string filePath)
|
||||||
{
|
{
|
||||||
if(File.Exists(filePath))
|
if(File.Exists(filePath))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Settings settings = JsonSerializer.Deserialize<Settings>(File.ReadAllText(filePath));
|
SettingsViewModel settings = JsonSerializer.Deserialize<SettingsViewModel>(File.ReadAllText(filePath));
|
||||||
settings._filePath = filePath;
|
settings._filePath = filePath;
|
||||||
|
|
||||||
MainWindow.ApplyTheme(settings.SelectedTheme);
|
|
||||||
|
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
catch(JsonException)
|
catch(JsonException)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Couldn't parse settings, reverting to default");
|
Console.WriteLine("Couldn't parse settings, reverting to default");
|
||||||
|
|
||||||
return new Settings(filePath);
|
return new SettingsViewModel(filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Settings(filePath);
|
return new SettingsViewModel(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save settings to a file
|
/// Apply settings from the UI
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Save()
|
public void ExecuteApplySettings()
|
||||||
{
|
{
|
||||||
|
if(!string.IsNullOrWhiteSpace(SelectedTheme))
|
||||||
|
App.PlayerView?.ViewModel?.ApplyTheme(SelectedTheme);
|
||||||
|
|
||||||
var options = new JsonSerializerOptions
|
var options = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
WriteIndented = true
|
WriteIndented = true
|
||||||
@@ -194,5 +240,45 @@ namespace RedBookPlayer.GUI
|
|||||||
string json = JsonSerializer.Serialize(this, options);
|
string json = JsonSerializer.Serialize(this, options);
|
||||||
File.WriteAllText(_filePath, json);
|
File.WriteAllText(_filePath, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Generation
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate the list of DataPlayback values
|
||||||
|
/// </summary>
|
||||||
|
private List<DataPlayback> GenerateDataPlaybackList() => Enum.GetValues(typeof(DataPlayback)).Cast<DataPlayback>().ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate the list of Key values
|
||||||
|
/// </summary>
|
||||||
|
private List<Key> GenerateKeyList() => Enum.GetValues(typeof(Key)).Cast<Key>().ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate the list of valid themes
|
||||||
|
/// </summary>
|
||||||
|
private List<string> GenerateThemeList()
|
||||||
|
{
|
||||||
|
// Create a list of all found themes
|
||||||
|
List<string> items = new List<string>();
|
||||||
|
|
||||||
|
// Ensure the theme directory exists
|
||||||
|
if(!Directory.Exists("themes/"))
|
||||||
|
Directory.CreateDirectory("themes/");
|
||||||
|
|
||||||
|
// Add all theme directories if they're valid
|
||||||
|
foreach(string dir in Directory.EnumerateDirectories("themes/"))
|
||||||
|
{
|
||||||
|
string themeName = dir.Split('/')[1];
|
||||||
|
|
||||||
|
if(!File.Exists($"themes/{themeName}/view.xaml"))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
items.Add(themeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,17 @@
|
|||||||
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
<ReactiveWindow xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
|
||||||
|
xmlns:rxui="clr-namespace:Avalonia;assembly=Avalonia.ReactiveUI"
|
||||||
|
xmlns:viewModels="clr-namespace:RedBookPlayer.GUI.ViewModels;assembly=RedBookPlayer.GUI"
|
||||||
|
xmlns:views="clr-namespace:RedBookPlayer.GUI.Views;assembly=RedBookPlayer.GUI"
|
||||||
x:Class="RedBookPlayer.GUI.Views.MainWindow" Title="RedBookPlayer" SizeToContent="WidthAndHeight"
|
x:Class="RedBookPlayer.GUI.Views.MainWindow" Title="RedBookPlayer" SizeToContent="WidthAndHeight"
|
||||||
DragDrop.AllowDrop="True">
|
CanResize="False" DragDrop.AllowDrop="True">
|
||||||
<ContentControl Name="Content" />
|
<ReactiveWindow.ViewModel>
|
||||||
</Window>
|
<viewModels:MainViewModel/>
|
||||||
|
</ReactiveWindow.ViewModel>
|
||||||
|
<ContentControl Name="Content">
|
||||||
|
<ContentControl.Content>
|
||||||
|
<views:PlayerView/>
|
||||||
|
</ContentControl.Content>
|
||||||
|
</ContentControl>
|
||||||
|
</ReactiveWindow>
|
||||||
@@ -1,73 +1,13 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Xml;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
using RedBookPlayer.GUI.ViewModels;
|
using RedBookPlayer.GUI.ViewModels;
|
||||||
|
|
||||||
namespace RedBookPlayer.GUI.Views
|
namespace RedBookPlayer.GUI.Views
|
||||||
{
|
{
|
||||||
public class MainWindow : Window
|
public class MainWindow : ReactiveWindow<MainViewModel>
|
||||||
{
|
{
|
||||||
public static MainWindow Instance;
|
public MainWindow() => InitializeComponent();
|
||||||
public ContentControl ContentControl;
|
|
||||||
public Window settingsWindow;
|
|
||||||
|
|
||||||
public MainWindow()
|
|
||||||
{
|
|
||||||
Instance = this;
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Apply a custom theme to the player
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="theme">Path to the theme under the themes directory</param>
|
|
||||||
public static void ApplyTheme(string theme)
|
|
||||||
{
|
|
||||||
// If no theme path is provided, we can ignore
|
|
||||||
if(string.IsNullOrWhiteSpace(theme))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// If we already have a view, cache the view model
|
|
||||||
PlayerViewModel pvm = ((PlayerView)Instance.ContentControl.Content).PlayerViewModel;
|
|
||||||
|
|
||||||
// If the theme name is "default", we assume the internal theme is used
|
|
||||||
if(theme.Equals("default", StringComparison.CurrentCultureIgnoreCase))
|
|
||||||
{
|
|
||||||
Instance.ContentControl.Content = new PlayerView(pvm);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
string themeDirectory = $"{Directory.GetCurrentDirectory()}/themes/{theme}";
|
|
||||||
string xamlPath = $"{themeDirectory}/view.xaml";
|
|
||||||
|
|
||||||
if(!File.Exists(xamlPath))
|
|
||||||
{
|
|
||||||
Console.WriteLine("Warning: specified theme doesn't exist, reverting to default");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string xaml = File.ReadAllText(xamlPath);
|
|
||||||
xaml = xaml.Replace("Source=\"", $"Source=\"file://{themeDirectory}/");
|
|
||||||
Instance.ContentControl.Content = new PlayerView(xaml, pvm);
|
|
||||||
}
|
|
||||||
catch(XmlException ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Error: invalid theme XAML ({ex.Message}), reverting to default");
|
|
||||||
Instance.ContentControl.Content = new PlayerView(pvm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Instance.Width = ((PlayerView)Instance.ContentControl.Content).Width;
|
|
||||||
Instance.Height = ((PlayerView)Instance.ContentControl.Content).Height;
|
|
||||||
|
|
||||||
pvm.InitializeDigits();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialize the main window
|
/// Initialize the main window
|
||||||
@@ -76,162 +16,10 @@ namespace RedBookPlayer.GUI.Views
|
|||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
|
||||||
ContentControl = this.FindControl<ContentControl>("Content");
|
// Add handlers
|
||||||
ContentControl.Content = new PlayerView();
|
Closing += ViewModel.ExecuteStop;
|
||||||
|
AddHandler(DragDrop.DropEvent, ViewModel.ExecuteLoadDragDrop);
|
||||||
Instance.MaxWidth = ((PlayerView)Instance.ContentControl.Content).Width;
|
KeyDown += ViewModel.ExecuteKeyPress;
|
||||||
Instance.MaxHeight = ((PlayerView)Instance.ContentControl.Content).Height;
|
|
||||||
|
|
||||||
ContentControl.Content = new PlayerView();
|
|
||||||
|
|
||||||
((PlayerView)ContentControl.Content).PlayerViewModel.InitializeDigits();
|
|
||||||
|
|
||||||
CanResize = false;
|
|
||||||
|
|
||||||
KeyDown += OnKeyDown;
|
|
||||||
|
|
||||||
Closing += (s, e) =>
|
|
||||||
{
|
|
||||||
settingsWindow?.Close();
|
|
||||||
settingsWindow = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
Closing += (e, f) =>
|
|
||||||
{
|
|
||||||
((PlayerView)ContentControl.Content).PlayerViewModel.ExecuteStop();
|
|
||||||
};
|
|
||||||
|
|
||||||
AddHandler(DragDrop.DropEvent, MainWindow_Drop);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Event Handlers
|
|
||||||
|
|
||||||
public async void MainWindow_Drop(object sender, DragEventArgs e)
|
|
||||||
{
|
|
||||||
PlayerView playerView = ContentControl.Content as PlayerView;
|
|
||||||
if(playerView == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
IEnumerable<string> fileNames = e.Data.GetFileNames();
|
|
||||||
foreach(string filename in fileNames)
|
|
||||||
{
|
|
||||||
bool loaded = await playerView?.PlayerViewModel?.LoadImage(filename);
|
|
||||||
if(loaded)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnKeyDown(object sender, KeyEventArgs e)
|
|
||||||
{
|
|
||||||
PlayerView playerView = ContentControl.Content as PlayerView;
|
|
||||||
|
|
||||||
// Open settings window
|
|
||||||
if(e.Key == App.Settings.OpenSettingsKey)
|
|
||||||
{
|
|
||||||
settingsWindow = new SettingsWindow(App.Settings);
|
|
||||||
settingsWindow.Closed += OnSettingsClosed;
|
|
||||||
settingsWindow.Show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load image
|
|
||||||
else if (e.Key == App.Settings.LoadImageKey)
|
|
||||||
{
|
|
||||||
playerView?.PlayerViewModel?.ExecuteLoad();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle playback
|
|
||||||
else if(e.Key == App.Settings.TogglePlaybackKey || e.Key == Key.MediaPlayPause)
|
|
||||||
{
|
|
||||||
playerView?.PlayerViewModel?.ExecuteTogglePlayPause();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop playback
|
|
||||||
else if(e.Key == App.Settings.StopPlaybackKey || e.Key == Key.MediaStop)
|
|
||||||
{
|
|
||||||
playerView?.PlayerViewModel?.ExecuteStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next Track
|
|
||||||
else if(e.Key == App.Settings.NextTrackKey || e.Key == Key.MediaNextTrack)
|
|
||||||
{
|
|
||||||
playerView?.PlayerViewModel?.ExecuteNextTrack();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Previous Track
|
|
||||||
else if(e.Key == App.Settings.PreviousTrackKey || e.Key == Key.MediaPreviousTrack)
|
|
||||||
{
|
|
||||||
playerView?.PlayerViewModel?.ExecutePreviousTrack();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next Index
|
|
||||||
else if(e.Key == App.Settings.NextIndexKey)
|
|
||||||
{
|
|
||||||
playerView?.PlayerViewModel?.ExecuteNextIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Previous Index
|
|
||||||
else if(e.Key == App.Settings.PreviousIndexKey)
|
|
||||||
{
|
|
||||||
playerView?.PlayerViewModel?.ExecutePreviousIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fast Foward
|
|
||||||
else if(e.Key == App.Settings.FastForwardPlaybackKey)
|
|
||||||
{
|
|
||||||
playerView?.PlayerViewModel?.ExecuteFastForward();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rewind
|
|
||||||
else if(e.Key == App.Settings.RewindPlaybackKey)
|
|
||||||
{
|
|
||||||
playerView?.PlayerViewModel?.ExecuteRewind();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Volume Up
|
|
||||||
else if(e.Key == App.Settings.VolumeUpKey || e.Key == Key.VolumeUp)
|
|
||||||
{
|
|
||||||
int increment = 1;
|
|
||||||
if(e.KeyModifiers.HasFlag(KeyModifiers.Control))
|
|
||||||
increment *= 2;
|
|
||||||
if(e.KeyModifiers.HasFlag(KeyModifiers.Shift))
|
|
||||||
increment *= 5;
|
|
||||||
|
|
||||||
if(playerView?.PlayerViewModel?.Volume != null)
|
|
||||||
playerView.PlayerViewModel.ExecuteSetVolume(playerView.PlayerViewModel.Volume + increment);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Volume Down
|
|
||||||
else if(e.Key == App.Settings.VolumeDownKey || e.Key == Key.VolumeDown)
|
|
||||||
{
|
|
||||||
int decrement = 1;
|
|
||||||
if(e.KeyModifiers.HasFlag(KeyModifiers.Control))
|
|
||||||
decrement *= 2;
|
|
||||||
if(e.KeyModifiers.HasFlag(KeyModifiers.Shift))
|
|
||||||
decrement *= 5;
|
|
||||||
|
|
||||||
if (playerView?.PlayerViewModel?.Volume != null)
|
|
||||||
playerView.PlayerViewModel.ExecuteSetVolume(playerView.PlayerViewModel.Volume - decrement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mute Toggle
|
|
||||||
else if(e.Key == App.Settings.ToggleMuteKey || e.Key == Key.VolumeMute)
|
|
||||||
{
|
|
||||||
playerView?.PlayerViewModel?.ExecuteToggleMute();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emphasis Toggle
|
|
||||||
else if(e.Key == App.Settings.ToggleDeEmphasisKey)
|
|
||||||
{
|
|
||||||
playerView?.PlayerViewModel?.ExecuteToggleDeEmphasis();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnSettingsClosed(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
PlayerView playerView = ContentControl.Content as PlayerView;
|
|
||||||
playerView?.UpdateViewModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
<ReactiveUserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
|
||||||
x:Class="RedBookPlayer.GUI.Views.PlayerView" Width="900" Height="400">
|
xmlns:rxui="clr-namespace:Avalonia;assembly=Avalonia.ReactiveUI"
|
||||||
|
xmlns:viewModels="clr-namespace:RedBookPlayer.GUI.ViewModels;assembly=RedBookPlayer.GUI"
|
||||||
|
x:Class="RedBookPlayer.GUI.Views.PlayerView" Width="900" Height="400" Background="White">
|
||||||
|
<ReactiveUserControl.ViewModel>
|
||||||
|
<viewModels:PlayerViewModel/>
|
||||||
|
</ReactiveUserControl.ViewModel>
|
||||||
<StackPanel Margin="16" VerticalAlignment="Center">
|
<StackPanel Margin="16" VerticalAlignment="Center">
|
||||||
<Button Command="{Binding LoadCommand}" Margin="32,0,32,16">Load</Button>
|
<Button Command="{Binding LoadCommand}" Margin="32,0,32,16">Load</Button>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
@@ -101,4 +106,4 @@
|
|||||||
<TextBlock Margin="0,0,16,0" Text="{Binding Volume}"/>
|
<TextBlock Margin="0,0,16,0" Text="{Binding Volume}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UserControl>
|
</ReactiveUserControl>
|
||||||
@@ -1,73 +1,9 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.ReactiveUI;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using RedBookPlayer.GUI.ViewModels;
|
using RedBookPlayer.GUI.ViewModels;
|
||||||
|
|
||||||
namespace RedBookPlayer.GUI.Views
|
namespace RedBookPlayer.GUI.Views
|
||||||
{
|
{
|
||||||
public class PlayerView : UserControl
|
public class PlayerView : ReactiveUserControl<PlayerViewModel>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Read-only access to the view model
|
|
||||||
/// </summary>
|
|
||||||
public PlayerViewModel PlayerViewModel => DataContext as PlayerViewModel;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize the UI based on the default theme
|
|
||||||
/// </summary>
|
|
||||||
public PlayerView() : this(null, null) { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize the UI based on the default theme with an existing view model
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="xaml">XAML data representing the theme, null for default</param>
|
|
||||||
/// <param name="playerViewModel">Existing PlayerViewModel to load in instead of creating a new one</param>
|
|
||||||
public PlayerView(PlayerViewModel playerViewModel) : this(null, playerViewModel) { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize the UI based on the currently selected theme
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="xaml">XAML data representing the theme, null for default</param>
|
|
||||||
/// <param name="playerViewModel">Existing PlayerViewModel to load in instead of creating a new one</param>
|
|
||||||
public PlayerView(string xaml, PlayerViewModel playerViewModel)
|
|
||||||
{
|
|
||||||
LoadTheme(xaml);
|
|
||||||
|
|
||||||
if(playerViewModel != null)
|
|
||||||
DataContext = playerViewModel;
|
|
||||||
else
|
|
||||||
DataContext = new PlayerViewModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Helpers
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update the view model with new settings
|
|
||||||
/// </summary>
|
|
||||||
public void UpdateViewModel()
|
|
||||||
{
|
|
||||||
PlayerViewModel.SetLoadDataTracks(App.Settings.PlayDataTracks);
|
|
||||||
PlayerViewModel.SetLoadHiddenTracks(App.Settings.PlayHiddenTracks);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Load the theme from a XAML, if possible
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="xaml">XAML data representing the theme, null for default</param>
|
|
||||||
private void LoadTheme(string xaml)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if(xaml != null)
|
|
||||||
new AvaloniaXamlLoader().Load(xaml, null, this);
|
|
||||||
else
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
<ReactiveWindow xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
|
||||||
|
xmlns:rxui="clr-namespace:Avalonia;assembly=Avalonia.ReactiveUI" d:DesignWidth="800"
|
||||||
|
xmlns:viewModels="clr-namespace:RedBookPlayer.GUI.ViewModels;assembly=RedBookPlayer.GUI"
|
||||||
d:DesignHeight="450" x:Class="RedBookPlayer.GUI.Views.SettingsWindow" Title="Settings" SizeToContent="WidthAndHeight">
|
d:DesignHeight="450" x:Class="RedBookPlayer.GUI.Views.SettingsWindow" Title="Settings" SizeToContent="WidthAndHeight">
|
||||||
|
<ReactiveWindow.ViewModel>
|
||||||
|
<viewModels:SettingsViewModel/>
|
||||||
|
</ReactiveWindow.ViewModel>
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TabControl>
|
<TabControl>
|
||||||
<TabItem Header="UI Settings">
|
<TabItem Header="UI Settings">
|
||||||
@@ -21,8 +26,9 @@
|
|||||||
<TextBlock VerticalAlignment="Center">Play hidden tracks</TextBlock>
|
<TextBlock VerticalAlignment="Center">Play hidden tracks</TextBlock>
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
<WrapPanel Margin="0,0,0,16">
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<CheckBox IsChecked="{Binding PlayDataTracks}" Margin="0,0,8,0"/>
|
<TextBlock Grid.Row="0" Grid.Column="0" Width="120">Data Track Playback</TextBlock>
|
||||||
<TextBlock VerticalAlignment="Center">Play data tracks like old, non-compliant players</TextBlock>
|
<ComboBox Grid.Row="0" Grid.Column="1" Name="DataPlayback" Margin="8,0,0,0" Width="120"
|
||||||
|
Items="{Binding DataPlaybackValues}" SelectedItem="{Binding DataPlayback, Mode=TwoWay}" />
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
<WrapPanel Margin="0,0,0,16">
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<CheckBox IsChecked="{Binding GenerateMissingTOC}" Margin="0,0,8,0"/>
|
<CheckBox IsChecked="{Binding GenerateMissingTOC}" Margin="0,0,8,0"/>
|
||||||
@@ -33,11 +39,12 @@
|
|||||||
<TextBlock VerticalAlignment="Center" DockPanel.Dock="Right" Text="%" />
|
<TextBlock VerticalAlignment="Center" DockPanel.Dock="Right" Text="%" />
|
||||||
<TextBlock VerticalAlignment="Center" Margin="8,0,0,0" DockPanel.Dock="Right" Text="{Binding Volume}"
|
<TextBlock VerticalAlignment="Center" Margin="8,0,0,0" DockPanel.Dock="Right" Text="{Binding Volume}"
|
||||||
Name="VolumeLabel" />
|
Name="VolumeLabel" />
|
||||||
<Slider Minimum="0" Maximum="100" SmallChange="1" LargeChange="10" Value="{Binding Volume}"
|
<Slider Minimum="0" Maximum="100" SmallChange="1" LargeChange="10" Value="{Binding Volume, Mode=TwoWay}"
|
||||||
Name="VolumeSlider" />
|
Name="VolumeSlider"/>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<ListBox Name="ThemeList" SelectionMode="Single" Margin="0,0,0,16" />
|
<ListBox Name="ThemeList" SelectionMode="Single" Margin="0,0,0,16"
|
||||||
|
Items="{Binding ThemeValues}" SelectedItem="{Binding SelectedTheme, Mode=TwoWay}"/>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem Header="Keyboard Bindings">
|
<TabItem Header="Keyboard Bindings">
|
||||||
@@ -61,62 +68,96 @@
|
|||||||
<RowDefinition/>
|
<RowDefinition/>
|
||||||
<RowDefinition/>
|
<RowDefinition/>
|
||||||
<RowDefinition/>
|
<RowDefinition/>
|
||||||
|
<RowDefinition/>
|
||||||
|
<RowDefinition/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- Load Image-->
|
<!-- Load Image-->
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Width="120">Load Image</TextBlock>
|
<TextBlock Grid.Row="0" Grid.Column="0" Width="120">Load Image</TextBlock>
|
||||||
<ComboBox Grid.Row="0" Grid.Column="1" HorizontalAlignment="Right" Name="LoadImageKeyBind" Margin="8,0,0,0" Width="120"/>
|
<ComboBox Grid.Row="0" Grid.Column="1" Name="LoadImageKeyBind"
|
||||||
|
Items="{Binding KeyboardList}" SelectedItem="{Binding LoadImageKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
|
||||||
<!-- Toggle Play/Pause -->
|
<!-- Toggle Play/Pause -->
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0" Width="120">Toggle Play/Pause</TextBlock>
|
<TextBlock Grid.Row="2" Grid.Column="0" Width="120">Toggle Play/Pause</TextBlock>
|
||||||
<ComboBox Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" Name="TogglePlaybackKeyBind" Margin="8,0,0,0" Width="120"/>
|
<ComboBox Grid.Row="2" Grid.Column="1" Name="TogglePlaybackKeyBind"
|
||||||
|
Items="{Binding KeyboardList}" SelectedItem="{Binding TogglePlaybackKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
|
||||||
<!-- Stop Playback-->
|
<!-- Stop Playback-->
|
||||||
<TextBlock Grid.Row="2" Grid.Column="0" Width="120">Stop Playback</TextBlock>
|
<TextBlock Grid.Row="3" Grid.Column="0" Width="120">Stop Playback</TextBlock>
|
||||||
<ComboBox Grid.Row="2" Grid.Column="1" HorizontalAlignment="Right" Name="StopPlaybackKeyBind" Margin="8,0,0,0" Width="120"/>
|
<ComboBox Grid.Row="3" Grid.Column="1" Name="StopPlaybackKeyBind"
|
||||||
|
Items="{Binding KeyboardList}" SelectedItem="{Binding StopPlaybackKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
|
||||||
|
<!-- Eject Disc-->
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="0" Width="120">Eject Disc</TextBlock>
|
||||||
|
<ComboBox Grid.Row="4" Grid.Column="1" Name="EjectKeyBind"
|
||||||
|
Items="{Binding KeyboardList}" SelectedItem="{Binding EjectKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
|
||||||
<!-- Next Track -->
|
<!-- Next Track -->
|
||||||
<TextBlock Grid.Row="3" Grid.Column="0" Width="120">Next Track</TextBlock>
|
<TextBlock Grid.Row="5" Grid.Column="0" Width="120">Next Track</TextBlock>
|
||||||
<ComboBox Grid.Row="3" Grid.Column="1" HorizontalAlignment="Right" Name="NextTrackKeyBind" Margin="8,0,0,0" Width="120"/>
|
<ComboBox Grid.Row="5" Grid.Column="1" Name="NextTrackKeyBind"
|
||||||
|
Items="{Binding KeyboardList}" SelectedItem="{Binding NextTrackKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
|
||||||
<!-- Previous Track -->
|
<!-- Previous Track -->
|
||||||
<TextBlock Grid.Row="4" Grid.Column="0" Width="120">Previous Track</TextBlock>
|
<TextBlock Grid.Row="6" Grid.Column="0" Width="120">Previous Track</TextBlock>
|
||||||
<ComboBox Grid.Row="4" Grid.Column="1" HorizontalAlignment="Right" Name="PreviousTrackKeyBind" Margin="8,0,0,0" Width="120"/>
|
<ComboBox Grid.Row="6" Grid.Column="1" Name="PreviousTrackKeyBind"
|
||||||
|
Items="{Binding KeyboardList}" SelectedItem="{Binding PreviousTrackKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
|
||||||
<!-- Next Index -->
|
<!-- Next Index -->
|
||||||
<TextBlock Grid.Row="5" Grid.Column="0" Width="120">Next Index</TextBlock>
|
<TextBlock Grid.Row="7" Grid.Column="0" Width="120">Next Index</TextBlock>
|
||||||
<ComboBox Grid.Row="5" Grid.Column="1" HorizontalAlignment="Right" Name="NextIndexKeyBind" Margin="8,0,0,0" Width="120"/>
|
<ComboBox Grid.Row="7" Grid.Column="1" Name="NextIndexKeyBind"
|
||||||
|
Items="{Binding KeyboardList}" SelectedItem="{Binding NextIndexKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
|
||||||
<!-- Previous Index -->
|
<!-- Previous Index -->
|
||||||
<TextBlock Grid.Row="6" Grid.Column="0" Width="120">Previous Index</TextBlock>
|
<TextBlock Grid.Row="8" Grid.Column="0" Width="120">Previous Index</TextBlock>
|
||||||
<ComboBox Grid.Row="6" Grid.Column="1" HorizontalAlignment="Right" Name="PreviousIndexKeyBind" Margin="8,0,0,0" Width="120"/>
|
<ComboBox Grid.Row="8" Grid.Column="1" Name="PreviousIndexKeyBind"
|
||||||
|
Items="{Binding KeyboardList}" SelectedItem="{Binding PreviousIndexKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
|
||||||
<!-- Fast Forward -->
|
<!-- Fast Forward -->
|
||||||
<TextBlock Grid.Row="7" Grid.Column="0" Width="120">Fast-Forward</TextBlock>
|
<TextBlock Grid.Row="9" Grid.Column="0" Width="120">Fast-Forward</TextBlock>
|
||||||
<ComboBox Grid.Row="7" Grid.Column="1" HorizontalAlignment="Right" Name="FastForwardPlaybackKeyBind" Margin="8,0,0,0" Width="120"/>
|
<ComboBox Grid.Row="9" Grid.Column="1" Name="FastForwardPlaybackKeyBind"
|
||||||
|
Items="{Binding KeyboardList}" SelectedItem="{Binding FastForwardPlaybackKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
|
||||||
<!-- Rewind -->
|
<!-- Rewind -->
|
||||||
<TextBlock Grid.Row="8" Grid.Column="0" Width="120">Rewind</TextBlock>
|
<TextBlock Grid.Row="10" Grid.Column="0" Width="120">Rewind</TextBlock>
|
||||||
<ComboBox Grid.Row="8" Grid.Column="1" HorizontalAlignment="Right" Name="RewindPlaybackKeyBind" Margin="8,0,0,0" Width="120"/>
|
<ComboBox Grid.Row="10" Grid.Column="1" Name="RewindPlaybackKeyBind"
|
||||||
|
Items="{Binding KeyboardList}" SelectedItem="{Binding RewindPlaybackKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
|
||||||
<!-- Volume Up -->
|
<!-- Volume Up -->
|
||||||
<TextBlock Grid.Row="9" Grid.Column="0" Width="120">Volume Up</TextBlock>
|
<TextBlock Grid.Row="11" Grid.Column="0" Width="120">Volume Up</TextBlock>
|
||||||
<ComboBox Grid.Row="9" Grid.Column="1" HorizontalAlignment="Right" Name="VolumeUpKeyBind" Margin="8,0,0,0" Width="120"/>
|
<ComboBox Grid.Row="11" Grid.Column="1" Name="VolumeUpKeyBind"
|
||||||
|
Items="{Binding KeyboardList}" SelectedItem="{Binding VolumeUpKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
|
||||||
<!-- Volume Down -->
|
<!-- Volume Down -->
|
||||||
<TextBlock Grid.Row="10" Grid.Column="0" Width="120">Volume Down</TextBlock>
|
<TextBlock Grid.Row="12" Grid.Column="0" Width="120">Volume Down</TextBlock>
|
||||||
<ComboBox Grid.Row="10" Grid.Column="1" HorizontalAlignment="Right" Name="VolumeDownKeyBind" Margin="8,0,0,0" Width="120"/>
|
<ComboBox Grid.Row="12" Grid.Column="1" Name="VolumeDownKeyBind"
|
||||||
|
Items="{Binding KeyboardList}" SelectedItem="{Binding VolumeDownKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
|
||||||
<!-- Mute Toggle -->
|
<!-- Mute Toggle -->
|
||||||
<TextBlock Grid.Row="11" Grid.Column="0" Width="120">Toggle Mute</TextBlock>
|
<TextBlock Grid.Row="13" Grid.Column="0" Width="120">Toggle Mute</TextBlock>
|
||||||
<ComboBox Grid.Row="11" Grid.Column="1" HorizontalAlignment="Right" Name="ToggleMuteKeyBind" Margin="8,0,0,0" Width="120"/>
|
<ComboBox Grid.Row="13" Grid.Column="1" Name="ToggleMuteKeyBind"
|
||||||
|
Items="{Binding KeyboardList}" SelectedItem="{Binding ToggleMuteKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
|
|
||||||
<!-- De-Emphasis Toggle -->
|
<!-- De-Emphasis Toggle -->
|
||||||
<TextBlock Grid.Row="12" Grid.Column="0" Width="120">Toggle De-Emphasis</TextBlock>
|
<TextBlock Grid.Row="14" Grid.Column="0" Width="120">Toggle De-Emphasis</TextBlock>
|
||||||
<ComboBox Grid.Row="12" Grid.Column="1" HorizontalAlignment="Right" Name="ToggleDeEmphasisKeyBind" Margin="8,0,0,0" Width="120"/>
|
<ComboBox Grid.Row="14" Grid.Column="1" Name="ToggleDeEmphasisKeyBind"
|
||||||
|
Items="{Binding KeyboardList}" SelectedItem="{Binding ToggleDeEmphasisKey, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" Margin="8,0,0,0" Width="120"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
<Button Name="ApplyButton">Apply</Button>
|
<Button Name="ApplyButton" Command="{Binding ApplySettingsCommand}">Apply</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Window>
|
</ReactiveWindow>
|
||||||
@@ -1,186 +1,9 @@
|
|||||||
using System;
|
using Avalonia.ReactiveUI;
|
||||||
using System.Collections.Generic;
|
using RedBookPlayer.GUI.ViewModels;
|
||||||
using System.IO;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Input;
|
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace RedBookPlayer.GUI.Views
|
namespace RedBookPlayer.GUI.Views
|
||||||
{
|
{
|
||||||
public class SettingsWindow : Window
|
public class SettingsWindow : ReactiveWindow<SettingsViewModel>
|
||||||
{
|
{
|
||||||
private readonly Settings _settings;
|
|
||||||
private string _selectedTheme;
|
|
||||||
private ListBox _themeList;
|
|
||||||
|
|
||||||
public SettingsWindow() {}
|
|
||||||
|
|
||||||
public SettingsWindow(Settings settings)
|
|
||||||
{
|
|
||||||
DataContext = _settings = settings;
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ThemeList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.AddedItems.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_selectedTheme = (string)e.AddedItems[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ApplySettings(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(_selectedTheme))
|
|
||||||
{
|
|
||||||
_settings.SelectedTheme = _selectedTheme;
|
|
||||||
MainWindow.ApplyTheme(_selectedTheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
SaveKeyboardList();
|
|
||||||
_settings.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateView() => this.FindControl<TextBlock>("VolumeLabel").Text = _settings.Volume.ToString();
|
|
||||||
|
|
||||||
void InitializeComponent()
|
|
||||||
{
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
|
|
||||||
PopulateThemes();
|
|
||||||
PopulateKeyboardList();
|
|
||||||
|
|
||||||
this.FindControl<Button>("ApplyButton").Click += ApplySettings;
|
|
||||||
this.FindControl<Slider>("VolumeSlider").PropertyChanged += (s, e) => UpdateView();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Populate the list of themes
|
|
||||||
/// </summary>
|
|
||||||
private void PopulateThemes()
|
|
||||||
{
|
|
||||||
// Get a reference to the theme list
|
|
||||||
_themeList = this.FindControl<ListBox>("ThemeList");
|
|
||||||
_themeList.SelectionChanged += ThemeList_SelectionChanged;
|
|
||||||
|
|
||||||
// Create a list of all found themes
|
|
||||||
List<string> items = new List<string>();
|
|
||||||
items.Add("default");
|
|
||||||
|
|
||||||
// Ensure the theme directory exists
|
|
||||||
if(!Directory.Exists("themes/"))
|
|
||||||
Directory.CreateDirectory("themes/");
|
|
||||||
|
|
||||||
// Add all theme directories if they're valid
|
|
||||||
foreach(string dir in Directory.EnumerateDirectories("themes/"))
|
|
||||||
{
|
|
||||||
string themeName = dir.Split('/')[1];
|
|
||||||
|
|
||||||
if(!File.Exists($"themes/{themeName}/view.xaml"))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
items.Add(themeName);
|
|
||||||
}
|
|
||||||
|
|
||||||
_themeList.Items = items;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Populate all of the keyboard bindings
|
|
||||||
/// </summary>
|
|
||||||
private void PopulateKeyboardList()
|
|
||||||
{
|
|
||||||
// Access all of the combo boxes
|
|
||||||
ComboBox loadImageKeyBind = this.FindControl<ComboBox>("LoadImageKeyBind");
|
|
||||||
ComboBox togglePlaybackKeyBind = this.FindControl<ComboBox>("TogglePlaybackKeyBind");
|
|
||||||
ComboBox stopPlaybackKeyBind = this.FindControl<ComboBox>("StopPlaybackKeyBind");
|
|
||||||
ComboBox nextTrackKeyBind = this.FindControl<ComboBox>("NextTrackKeyBind");
|
|
||||||
ComboBox previousTrackKeyBind = this.FindControl<ComboBox>("PreviousTrackKeyBind");
|
|
||||||
ComboBox nextIndexKeyBind = this.FindControl<ComboBox>("NextIndexKeyBind");
|
|
||||||
ComboBox previousIndexKeyBind = this.FindControl<ComboBox>("PreviousIndexKeyBind");
|
|
||||||
ComboBox fastForwardPlaybackKeyBind = this.FindControl<ComboBox>("FastForwardPlaybackKeyBind");
|
|
||||||
ComboBox rewindPlaybackKeyBind = this.FindControl<ComboBox>("RewindPlaybackKeyBind");
|
|
||||||
ComboBox volumeUpKeyBind = this.FindControl<ComboBox>("VolumeUpKeyBind");
|
|
||||||
ComboBox volumeDownKeyBind = this.FindControl<ComboBox>("VolumeDownKeyBind");
|
|
||||||
ComboBox toggleMuteKeyBind = this.FindControl<ComboBox>("ToggleMuteKeyBind");
|
|
||||||
ComboBox toggleDeEmphasisKeyBind = this.FindControl<ComboBox>("ToggleDeEmphasisKeyBind");
|
|
||||||
|
|
||||||
// Assign the list of values to all of them
|
|
||||||
Array keyboardList = GenerateKeyboardList();
|
|
||||||
loadImageKeyBind.Items = keyboardList;
|
|
||||||
togglePlaybackKeyBind.Items = keyboardList;
|
|
||||||
stopPlaybackKeyBind.Items = keyboardList;
|
|
||||||
nextTrackKeyBind.Items = keyboardList;
|
|
||||||
previousTrackKeyBind.Items = keyboardList;
|
|
||||||
nextIndexKeyBind.Items = keyboardList;
|
|
||||||
previousIndexKeyBind.Items = keyboardList;
|
|
||||||
fastForwardPlaybackKeyBind.Items = keyboardList;
|
|
||||||
rewindPlaybackKeyBind.Items = keyboardList;
|
|
||||||
volumeUpKeyBind.Items = keyboardList;
|
|
||||||
volumeDownKeyBind.Items = keyboardList;
|
|
||||||
toggleMuteKeyBind.Items = keyboardList;
|
|
||||||
toggleDeEmphasisKeyBind.Items = keyboardList;
|
|
||||||
|
|
||||||
// Set all of the currently selected items
|
|
||||||
loadImageKeyBind.SelectedItem = _settings.LoadImageKey;
|
|
||||||
togglePlaybackKeyBind.SelectedItem = _settings.TogglePlaybackKey;
|
|
||||||
stopPlaybackKeyBind.SelectedItem = _settings.StopPlaybackKey;
|
|
||||||
nextTrackKeyBind.SelectedItem = _settings.NextTrackKey;
|
|
||||||
previousTrackKeyBind.SelectedItem = _settings.PreviousTrackKey;
|
|
||||||
nextIndexKeyBind.SelectedItem = _settings.NextIndexKey;
|
|
||||||
previousIndexKeyBind.SelectedItem = _settings.PreviousIndexKey;
|
|
||||||
fastForwardPlaybackKeyBind.SelectedItem = _settings.FastForwardPlaybackKey;
|
|
||||||
rewindPlaybackKeyBind.SelectedItem = _settings.RewindPlaybackKey;
|
|
||||||
volumeUpKeyBind.SelectedItem = _settings.VolumeUpKey;
|
|
||||||
volumeDownKeyBind.SelectedItem = _settings.VolumeDownKey;
|
|
||||||
toggleMuteKeyBind.SelectedItem = _settings.ToggleMuteKey;
|
|
||||||
toggleDeEmphasisKeyBind.SelectedItem = _settings.ToggleDeEmphasisKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Save back all values from keyboard bindings
|
|
||||||
/// </summary>
|
|
||||||
private void SaveKeyboardList()
|
|
||||||
{
|
|
||||||
// Access all of the combo boxes
|
|
||||||
ComboBox loadImageKeyBind = this.FindControl<ComboBox>("LoadImageKeyBind");
|
|
||||||
ComboBox togglePlaybackKeyBind = this.FindControl<ComboBox>("TogglePlaybackKeyBind");
|
|
||||||
ComboBox stopPlaybackKeyBind = this.FindControl<ComboBox>("StopPlaybackKeyBind");
|
|
||||||
ComboBox nextTrackKeyBind = this.FindControl<ComboBox>("NextTrackKeyBind");
|
|
||||||
ComboBox previousTrackKeyBind = this.FindControl<ComboBox>("PreviousTrackKeyBind");
|
|
||||||
ComboBox nextIndexKeyBind = this.FindControl<ComboBox>("NextIndexKeyBind");
|
|
||||||
ComboBox previousIndexKeyBind = this.FindControl<ComboBox>("PreviousIndexKeyBind");
|
|
||||||
ComboBox fastForwardPlaybackKeyBind = this.FindControl<ComboBox>("FastForwardPlaybackKeyBind");
|
|
||||||
ComboBox rewindPlaybackKeyBind = this.FindControl<ComboBox>("RewindPlaybackKeyBind");
|
|
||||||
ComboBox volumeUpKeyBind = this.FindControl<ComboBox>("VolumeUpKeyBind");
|
|
||||||
ComboBox volumeDownKeyBind = this.FindControl<ComboBox>("VolumeDownKeyBind");
|
|
||||||
ComboBox toggleMuteKeyBind = this.FindControl<ComboBox>("ToggleMuteKeyBind");
|
|
||||||
ComboBox toggleDeEmphasisKeyBind = this.FindControl<ComboBox>("ToggleDeEmphasisKeyBind");
|
|
||||||
|
|
||||||
// Set all of the currently selected items
|
|
||||||
_settings.LoadImageKey = (Key)loadImageKeyBind.SelectedItem;
|
|
||||||
_settings.TogglePlaybackKey = (Key)togglePlaybackKeyBind.SelectedItem;
|
|
||||||
_settings.StopPlaybackKey = (Key)stopPlaybackKeyBind.SelectedItem;
|
|
||||||
_settings.NextTrackKey = (Key)nextTrackKeyBind.SelectedItem;
|
|
||||||
_settings.PreviousTrackKey = (Key)previousTrackKeyBind.SelectedItem;
|
|
||||||
_settings.NextIndexKey = (Key)nextIndexKeyBind.SelectedItem;
|
|
||||||
_settings.PreviousIndexKey = (Key)previousIndexKeyBind.SelectedItem;
|
|
||||||
_settings.FastForwardPlaybackKey = (Key)fastForwardPlaybackKeyBind.SelectedItem;
|
|
||||||
_settings.RewindPlaybackKey = (Key)rewindPlaybackKeyBind.SelectedItem;
|
|
||||||
_settings.VolumeUpKey = (Key)volumeUpKeyBind.SelectedItem;
|
|
||||||
_settings.VolumeDownKey = (Key)volumeDownKeyBind.SelectedItem;
|
|
||||||
_settings.ToggleMuteKey = (Key)toggleMuteKeyBind.SelectedItem;
|
|
||||||
_settings.ToggleDeEmphasisKey = (Key)toggleDeEmphasisKeyBind.SelectedItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate a list of keyboard keys for mapping
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
private Array GenerateKeyboardList()
|
|
||||||
{
|
|
||||||
return Enum.GetValues(typeof(Key));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BIN
RedBookPlayer.GUI/themes/Default/-.png
Normal file
|
After Width: | Height: | Size: 357 B |
BIN
RedBookPlayer.GUI/themes/Default/0.png
Normal file
|
After Width: | Height: | Size: 658 B |
BIN
RedBookPlayer.GUI/themes/Default/1.png
Normal file
|
After Width: | Height: | Size: 414 B |
BIN
RedBookPlayer.GUI/themes/Default/2.png
Normal file
|
After Width: | Height: | Size: 604 B |
BIN
RedBookPlayer.GUI/themes/Default/3.png
Normal file
|
After Width: | Height: | Size: 595 B |
BIN
RedBookPlayer.GUI/themes/Default/4.png
Normal file
|
After Width: | Height: | Size: 550 B |
BIN
RedBookPlayer.GUI/themes/Default/5.png
Normal file
|
After Width: | Height: | Size: 601 B |
BIN
RedBookPlayer.GUI/themes/Default/6.png
Normal file
|
After Width: | Height: | Size: 656 B |
BIN
RedBookPlayer.GUI/themes/Default/7.png
Normal file
|
After Width: | Height: | Size: 547 B |
BIN
RedBookPlayer.GUI/themes/Default/8.png
Normal file
|
After Width: | Height: | Size: 693 B |
BIN
RedBookPlayer.GUI/themes/Default/9.png
Normal file
|
After Width: | Height: | Size: 648 B |
BIN
RedBookPlayer.GUI/themes/Default/colon.png
Normal file
|
After Width: | Height: | Size: 407 B |
109
RedBookPlayer.GUI/themes/Default/view.xaml
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<ReactiveUserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
|
||||||
|
xmlns:rxui="clr-namespace:Avalonia;assembly=Avalonia.ReactiveUI"
|
||||||
|
xmlns:viewModels="clr-namespace:RedBookPlayer.GUI.ViewModels;assembly=RedBookPlayer.GUI"
|
||||||
|
x:Class="RedBookPlayer.GUI.Views.PlayerView" Width="900" Height="400" Background="White">
|
||||||
|
<ReactiveUserControl.ViewModel>
|
||||||
|
<viewModels:PlayerViewModel/>
|
||||||
|
</ReactiveUserControl.ViewModel>
|
||||||
|
<StackPanel Margin="16" VerticalAlignment="Center">
|
||||||
|
<Button Command="{Binding LoadCommand}" Margin="32,0,32,16">Load</Button>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
|
<Button Command="{Binding PlayCommand}" Width="100" Margin="0,0,16,0">Play</Button>
|
||||||
|
<Button Command="{Binding PauseCommand}" Width="100" Margin="0,0,16,0">Pause</Button>
|
||||||
|
<Button Command="{Binding StopCommand}" Width="100" Margin="0,0,16,0">Stop</Button>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
|
<Button Command="{Binding PreviousTrackCommand}" Width="100" Margin="0,0,16,0">Previous Track</Button>
|
||||||
|
<Button Command="{Binding NextTrackCommand}" Width="100" Margin="0,0,16,0">Next Track</Button>
|
||||||
|
<Button Command="{Binding PreviousIndexCommand}" Width="100" Margin="0,0,16,0">Previous Index</Button>
|
||||||
|
<Button Command="{Binding NextIndexCommand}" Width="100" Margin="0,0,16,0">Next Index</Button>
|
||||||
|
<RepeatButton Command="{Binding RewindCommand}" Width="100" Margin="0,0,16,0">Rewind</RepeatButton>
|
||||||
|
<RepeatButton Command="{Binding FastForwardCommand}" Width="100">Fast Forward</RepeatButton>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
|
<StackPanel Margin="0,0,32,0">
|
||||||
|
<TextBlock Margin="0,0,0,4">TRACK</TextBlock>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||||
|
<Image Name="TrackDigit1" Width="42" Height="51" Source="/-.png" />
|
||||||
|
<Image Name="TrackDigit2" Width="42" Height="51" Source="/-.png" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Margin="0,0,32,0">
|
||||||
|
<TextBlock Margin="0,0,0,4">INDEX</TextBlock>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||||
|
<Image Name="IndexDigit1" Width="42" Height="51" Source="/-.png" />
|
||||||
|
<Image Name="IndexDigit2" Width="42" Height="51" Source="/-.png" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Margin="0,0,0,4">TIME</TextBlock>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||||
|
<Image Name="TimeDigit1" Width="42" Height="51" Source="/-.png" />
|
||||||
|
<Image Name="TimeDigit2" Width="42" Height="51" Source="/-.png" />
|
||||||
|
<Image Width="11" Height="51" Source="/colon.png" />
|
||||||
|
<Image Name="TimeDigit3" Width="42" Height="51" Source="/-.png" />
|
||||||
|
<Image Name="TimeDigit4" Width="42" Height="51" Source="/-.png" />
|
||||||
|
<Image Width="11" Height="51" Source="/colon.png" />
|
||||||
|
<Image Name="TimeDigit5" Width="42" Height="51" Source="/-.png" />
|
||||||
|
<Image Name="TimeDigit6" Width="42" Height="51" Source="/-.png" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
|
<StackPanel Margin="0,0,32,0">
|
||||||
|
<TextBlock Margin="0,0,0,4">TRACKS</TextBlock>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||||
|
<Image Name="TotalTracksDigit1" Width="42" Height="51" Source="/-.png" />
|
||||||
|
<Image Name="TotalTracksDigit2" Width="42" Height="51" Source="/-.png" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Margin="0,0,32,0">
|
||||||
|
<TextBlock Margin="0,0,0,4">INDEXES</TextBlock>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||||
|
<Image Name="TotalIndexesDigit1" Width="42" Height="51" Source="/-.png" />
|
||||||
|
<Image Name="TotalIndexesDigit2" Width="42" Height="51" Source="/-.png" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Margin="0,0,0,4">TOTAL</TextBlock>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||||
|
<Image Name="TotalTimeDigit1" Width="42" Height="51" Source="/-.png" />
|
||||||
|
<Image Name="TotalTimeDigit2" Width="42" Height="51" Source="/-.png" />
|
||||||
|
<Image Width="11" Height="51" Source="/colon.png" />
|
||||||
|
<Image Name="TotalTimeDigit3" Width="42" Height="51" Source="/-.png" />
|
||||||
|
<Image Name="TotalTimeDigit4" Width="42" Height="51" Source="/-.png" />
|
||||||
|
<Image Width="11" Height="51" Source="/colon.png" />
|
||||||
|
<Image Name="TotalTimeDigit5" Width="42" Height="51" Source="/-.png" />
|
||||||
|
<Image Name="TotalTimeDigit6" Width="42" Height="51" Source="/-.png" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
|
<Button Command="{Binding EnableDeEmphasisCommand}" IsVisible="{Binding !ApplyDeEmphasis}" Width="200"
|
||||||
|
Margin="0,0,16,0">
|
||||||
|
Enable De-Emphasis
|
||||||
|
</Button>
|
||||||
|
<Button Command="{Binding DisableDeEmphasisCommand}" IsVisible="{Binding ApplyDeEmphasis}" Width="200"
|
||||||
|
Margin="0,0,16,0">
|
||||||
|
Disable De-Emphasis
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||||
|
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding IsDataTrack}">AUDIO</TextBlock>
|
||||||
|
<TextBlock Margin="0,0,16,0" IsVisible="{Binding !IsDataTrack}">AUDIO</TextBlock>
|
||||||
|
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !IsDataTrack}">DATA</TextBlock>
|
||||||
|
<TextBlock Margin="0,0,16,0" IsVisible="{Binding IsDataTrack}">DATA</TextBlock>
|
||||||
|
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !TrackHasEmphasis}">EMPHASIS</TextBlock>
|
||||||
|
<TextBlock Margin="0,0,16,0" IsVisible="{Binding TrackHasEmphasis}">EMPHASIS</TextBlock>
|
||||||
|
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !CopyAllowed}">COPY</TextBlock>
|
||||||
|
<TextBlock Margin="0,0,16,0" IsVisible="{Binding CopyAllowed}">COPY</TextBlock>
|
||||||
|
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !QuadChannel}">4CH</TextBlock>
|
||||||
|
<TextBlock Margin="0,0,16,0" IsVisible="{Binding QuadChannel}">4CH</TextBlock>
|
||||||
|
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !HiddenTrack}">HIDDEN</TextBlock>
|
||||||
|
<TextBlock Margin="0,0,16,0" IsVisible="{Binding HiddenTrack}">HIDDEN</TextBlock>
|
||||||
|
<TextBlock Margin="0,0,16,0" Text="{Binding Volume}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</ReactiveUserControl>
|
||||||
@@ -26,7 +26,7 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// Data tracks only and flag disabled means we can't do anything
|
// Data tracks only and flag disabled means we can't do anything
|
||||||
if(_image.Tracks.All(t => t.TrackType != TrackType.Audio) && !LoadDataTracks)
|
if(_image.Tracks.All(t => t.TrackType != TrackType.Audio) && DataPlayback == DataPlayback.Skip)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Cache the value and the current track number
|
// Cache the value and the current track number
|
||||||
@@ -69,8 +69,10 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
SetTrackFlags(track);
|
SetTrackFlags(track);
|
||||||
|
|
||||||
// If the track is playable, just return
|
// If the track is playable, just return
|
||||||
if(TrackType == TrackType.Audio || LoadDataTracks)
|
if(TrackType == TrackType.Audio || DataPlayback != DataPlayback.Skip)
|
||||||
|
{
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// If we're not playing the track, skip
|
// If we're not playing the track, skip
|
||||||
if(increment)
|
if(increment)
|
||||||
@@ -143,26 +145,28 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
if(_image == null)
|
if(_image == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// If the sector is over the end of the image, then loop
|
||||||
|
ulong tempSector = value;
|
||||||
|
if(tempSector > _image.Info.Sectors)
|
||||||
|
tempSector = 0;
|
||||||
|
else if(tempSector < 0)
|
||||||
|
tempSector = _image.Info.Sectors - 1;
|
||||||
|
|
||||||
// Cache the current track for easy access
|
// Cache the current track for easy access
|
||||||
Track track = GetTrack(CurrentTrackNumber);
|
Track track = GetTrack(CurrentTrackNumber);
|
||||||
if(track == null)
|
if(track == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.RaiseAndSetIfChanged(ref _currentSector, value);
|
this.RaiseAndSetIfChanged(ref _currentSector, tempSector);
|
||||||
|
|
||||||
if((CurrentTrackNumber < _image.Tracks.Count - 1 && CurrentSector >= (GetTrack(CurrentTrackNumber + 1)?.TrackStartSector ?? 0))
|
// If the current sector is outside of the last known track, seek to the right one
|
||||||
|| (CurrentTrackNumber > 0 && CurrentSector < track.TrackStartSector))
|
if(CurrentSector < track.TrackStartSector || CurrentSector > track.TrackEndSector)
|
||||||
{
|
{
|
||||||
foreach(Track trackData in _image.Tracks.ToArray().Reverse())
|
track = _image.Tracks.Last(t => CurrentSector >= t.TrackStartSector);
|
||||||
{
|
CurrentTrackNumber = (int)track.TrackSequence;
|
||||||
if(CurrentSector >= trackData.TrackStartSector)
|
|
||||||
{
|
|
||||||
CurrentTrackNumber = (int)trackData.TrackSequence;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the new index, if necessary
|
||||||
foreach((ushort key, int i) in track.Indexes.Reverse())
|
foreach((ushort key, int i) in track.Indexes.Reverse())
|
||||||
{
|
{
|
||||||
if((int)CurrentSector >= i)
|
if((int)CurrentSector >= i)
|
||||||
@@ -216,9 +220,9 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicate if data tracks should be loaded
|
/// Indicate how data tracks should be handled
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool LoadDataTracks { get; set; } = false;
|
public DataPlayback DataPlayback { get; set; } = DataPlayback.Skip;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicate if hidden tracks should be loaded
|
/// Indicate if hidden tracks should be loaded
|
||||||
@@ -264,14 +268,12 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructor
|
/// Constructor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="generateMissingToc">Generate a TOC if the disc is missing one</param>
|
/// <param name="options">Set of options for a new disc</param>
|
||||||
/// <param name="loadHiddenTracks">Load hidden tracks for playback</param>
|
public CompactDisc(OpticalDiscOptions options)
|
||||||
/// <param name="loadDataTracks">Load data tracks for playback</param>
|
|
||||||
public CompactDisc(bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks)
|
|
||||||
{
|
{
|
||||||
_generateMissingToc = generateMissingToc;
|
DataPlayback = options.DataPlayback;
|
||||||
LoadHiddenTracks = loadHiddenTracks;
|
_generateMissingToc = options.GenerateMissingToc;
|
||||||
LoadDataTracks = loadDataTracks;
|
LoadHiddenTracks = options.LoadHiddenTracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -392,7 +394,7 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the previous index has an invalid offset, change tracks if needed
|
// If the previous index has an invalid offset, change tracks if needed
|
||||||
else if (track.Indexes[(ushort)(CurrentTrackIndex - 1)] < 0)
|
else if(track.Indexes[(ushort)(CurrentTrackIndex - 1)] < 0)
|
||||||
{
|
{
|
||||||
if(changeTrack)
|
if(changeTrack)
|
||||||
{
|
{
|
||||||
@@ -401,7 +403,7 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, just move to the previous index
|
// Otherwise, just move to the previous index
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -416,23 +418,7 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
#region Helpers
|
#region Helpers
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override void LoadFirstTrack()
|
public override void LoadTrack(int trackNumber)
|
||||||
{
|
|
||||||
CurrentTrackNumber = 1;
|
|
||||||
LoadTrack(CurrentTrackNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override void SetTotalIndexes()
|
|
||||||
{
|
|
||||||
if(_image == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
TotalIndexes = GetTrack(CurrentTrackNumber)?.Indexes.Keys.Max() ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
protected override void LoadTrack(int trackNumber)
|
|
||||||
{
|
{
|
||||||
if(_image == null)
|
if(_image == null)
|
||||||
return;
|
return;
|
||||||
@@ -448,6 +434,33 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
CurrentSector = (ulong)(track?.Indexes.OrderBy(kvp => kvp.Key).First(kvp => kvp.Value >= 0).Value ?? 0);
|
CurrentSector = (ulong)(track?.Indexes.OrderBy(kvp => kvp.Key).First(kvp => kvp.Value >= 0).Value ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void LoadFirstTrack()
|
||||||
|
{
|
||||||
|
CurrentTrackNumber = 1;
|
||||||
|
LoadTrack(CurrentTrackNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override byte[] ReadSectors(uint sectorsToRead)
|
||||||
|
{
|
||||||
|
if(TrackType == TrackType.Audio || DataPlayback == DataPlayback.Play)
|
||||||
|
return base.ReadSectors(sectorsToRead);
|
||||||
|
else if(DataPlayback == DataPlayback.Blank)
|
||||||
|
return new byte[sectorsToRead * BytesPerSector];
|
||||||
|
else
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void SetTotalIndexes()
|
||||||
|
{
|
||||||
|
if(_image == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TotalIndexes = GetTrack(CurrentTrackNumber)?.Indexes.Keys.Max() ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the track with the given sequence value, if possible
|
/// Get the track with the given sequence value, if possible
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -580,4 +593,4 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,6 +122,12 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
|
|
||||||
#region Helpers
|
#region Helpers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load the desired track, if possible
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="track">Track number to load</param>
|
||||||
|
public abstract void LoadTrack(int track);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load the first valid track in the image
|
/// Load the first valid track in the image
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -132,7 +138,7 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sectorsToRead">Current number of sectors to read</param>
|
/// <param name="sectorsToRead">Current number of sectors to read</param>
|
||||||
/// <returns>Byte array representing the read sectors, if possible</returns>
|
/// <returns>Byte array representing the read sectors, if possible</returns>
|
||||||
public byte[] ReadSectors(uint sectorsToRead) => _image.ReadSectors(CurrentSector, sectorsToRead);
|
public virtual byte[] ReadSectors(uint sectorsToRead) => _image.ReadSectors(CurrentSector, sectorsToRead);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the total indexes from the current track
|
/// Set the total indexes from the current track
|
||||||
@@ -145,12 +151,6 @@ namespace RedBookPlayer.Models.Discs
|
|||||||
/// <param name="sector">New sector number to use</param>
|
/// <param name="sector">New sector number to use</param>
|
||||||
public void SetCurrentSector(ulong sector) => CurrentSector = sector;
|
public void SetCurrentSector(ulong sector) => CurrentSector = sector;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Load the desired track, if possible
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="track">Track number to load</param>
|
|
||||||
protected abstract void LoadTrack(int track);
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
24
RedBookPlayer.Models/Discs/OpticalDiscOptions.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
namespace RedBookPlayer.Models.Discs
|
||||||
|
{
|
||||||
|
public class OpticalDiscOptions
|
||||||
|
{
|
||||||
|
#region CompactDisc
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicate how data tracks should be handled
|
||||||
|
/// </summary>
|
||||||
|
public DataPlayback DataPlayback { get; set; } = DataPlayback.Skip;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicate if a TOC should be generated if missing
|
||||||
|
/// </summary>
|
||||||
|
public bool GenerateMissingToc { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicate if hidden tracks should be loaded
|
||||||
|
/// </summary>
|
||||||
|
public bool LoadHiddenTracks { get; set; } = false;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
49
RedBookPlayer.Models/Enums.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
namespace RedBookPlayer.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Determine how to handle data tracks
|
||||||
|
/// </summary>
|
||||||
|
public enum DataPlayback
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Skip playing all data tracks
|
||||||
|
/// </summary>
|
||||||
|
Skip = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Play silence for all data tracks
|
||||||
|
/// </summary>
|
||||||
|
Blank = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Play the data from all data tracks
|
||||||
|
/// </summary>
|
||||||
|
Play = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current player state
|
||||||
|
/// </summary>
|
||||||
|
public enum PlayerState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// No disc is loaded
|
||||||
|
/// </summary>
|
||||||
|
NoDisc,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disc is loaded, playback is stopped
|
||||||
|
/// </summary>
|
||||||
|
Stopped,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disc is loaded, playback is paused
|
||||||
|
/// </summary>
|
||||||
|
Paused,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disc is loaded, playback enabled
|
||||||
|
/// </summary>
|
||||||
|
Playing,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,12 +13,10 @@ namespace RedBookPlayer.Models.Factories
|
|||||||
/// Generate an OpticalDisc from an input path
|
/// Generate an OpticalDisc from an input path
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Path to load the image from</param>
|
/// <param name="path">Path to load the image from</param>
|
||||||
/// <param name="generateMissingToc">Generate a TOC if the disc is missing one [CompactDisc only]</param>
|
/// <param name="options">Options to pass to the optical disc factory</param>
|
||||||
/// <param name="loadHiddenTracks">Load hidden tracks for playback [CompactDisc only]</param>
|
|
||||||
/// <param name="loadDataTracks">Load data tracks for playback [CompactDisc only]</param>
|
|
||||||
/// <param name="autoPlay">True if the image should be playable immediately, false otherwise</param>
|
/// <param name="autoPlay">True if the image should be playable immediately, false otherwise</param>
|
||||||
/// <returns>Instantiated OpticalDisc, if possible</returns>
|
/// <returns>Instantiated OpticalDisc, if possible</returns>
|
||||||
public static OpticalDiscBase GenerateFromPath(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay)
|
public static OpticalDiscBase GenerateFromPath(string path, OpticalDiscOptions options, bool autoPlay)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -34,7 +32,7 @@ namespace RedBookPlayer.Models.Factories
|
|||||||
image.Open(filter);
|
image.Open(filter);
|
||||||
|
|
||||||
// Generate and instantiate the disc
|
// Generate and instantiate the disc
|
||||||
return GenerateFromImage(image, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay);
|
return GenerateFromImage(image, options, autoPlay);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -47,12 +45,10 @@ namespace RedBookPlayer.Models.Factories
|
|||||||
/// Generate an OpticalDisc from an input IOpticalMediaImage
|
/// Generate an OpticalDisc from an input IOpticalMediaImage
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="image">IOpticalMediaImage to create from</param>
|
/// <param name="image">IOpticalMediaImage to create from</param>
|
||||||
/// <param name="generateMissingToc">Generate a TOC if the disc is missing one [CompactDisc only]</param>
|
/// <param name="options">Options to pass to the optical disc factory</param>
|
||||||
/// <param name="loadHiddenTracks">Load hidden tracks for playback [CompactDisc only]</param>
|
|
||||||
/// <param name="loadDataTracks">Load data tracks for playback [CompactDisc only]</param>
|
|
||||||
/// <param name="autoPlay">True if the image should be playable immediately, false otherwise</param>
|
/// <param name="autoPlay">True if the image should be playable immediately, false otherwise</param>
|
||||||
/// <returns>Instantiated OpticalDisc, if possible</returns>
|
/// <returns>Instantiated OpticalDisc, if possible</returns>
|
||||||
public static OpticalDiscBase GenerateFromImage(IOpticalMediaImage image, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay)
|
public static OpticalDiscBase GenerateFromImage(IOpticalMediaImage image, OpticalDiscOptions options, bool autoPlay)
|
||||||
{
|
{
|
||||||
// If the image is not usable, we don't do anything
|
// If the image is not usable, we don't do anything
|
||||||
if(!IsUsableImage(image))
|
if(!IsUsableImage(image))
|
||||||
@@ -66,7 +62,7 @@ namespace RedBookPlayer.Models.Factories
|
|||||||
{
|
{
|
||||||
case "Compact Disc":
|
case "Compact Disc":
|
||||||
case "GD":
|
case "GD":
|
||||||
opticalDisc = new CompactDisc(generateMissingToc, loadHiddenTracks, loadDataTracks);
|
opticalDisc = new CompactDisc(options);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
opticalDisc = null;
|
opticalDisc = null;
|
||||||
@@ -118,4 +114,4 @@ namespace RedBookPlayer.Models.Factories
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using Aaru.CommonTypes.Enums;
|
using Aaru.CommonTypes.Enums;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
@@ -12,7 +11,13 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicate if the player is ready to be used
|
/// Indicate if the player is ready to be used
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Initialized { get; private set; } = false;
|
public bool Initialized
|
||||||
|
{
|
||||||
|
get => _initialized;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _initialized, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _initialized;
|
||||||
|
|
||||||
#region OpticalDisc Passthrough
|
#region OpticalDisc Passthrough
|
||||||
|
|
||||||
@@ -100,27 +105,27 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the total tracks on the disc
|
/// Represents the total tracks on the disc
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int TotalTracks => _opticalDisc.TotalTracks;
|
public int TotalTracks => _opticalDisc?.TotalTracks ?? 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the total indices on the disc
|
/// Represents the total indices on the disc
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int TotalIndexes => _opticalDisc.TotalIndexes;
|
public int TotalIndexes => _opticalDisc?.TotalIndexes ?? 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Total sectors in the image
|
/// Total sectors in the image
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ulong TotalSectors => _opticalDisc.TotalSectors;
|
public ulong TotalSectors => _opticalDisc?.TotalSectors ?? 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the time adjustment offset for the disc
|
/// Represents the time adjustment offset for the disc
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ulong TimeOffset => _opticalDisc.TimeOffset;
|
public ulong TimeOffset => _opticalDisc?.TimeOffset ?? 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the total playing time for the disc
|
/// Represents the total playing time for the disc
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ulong TotalTime => _opticalDisc.TotalTime;
|
public ulong TotalTime => _opticalDisc?.TotalTime ?? 0;
|
||||||
|
|
||||||
private int _currentTrackNumber;
|
private int _currentTrackNumber;
|
||||||
private ushort _currentTrackIndex;
|
private ushort _currentTrackIndex;
|
||||||
@@ -138,12 +143,21 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
#region SoundOutput Passthrough
|
#region SoundOutput Passthrough
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicate if the output is playing
|
/// Indicates the current player state
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? Playing
|
public PlayerState PlayerState
|
||||||
{
|
{
|
||||||
get => _playing;
|
get => _playerState;
|
||||||
private set => this.RaiseAndSetIfChanged(ref _playing, value);
|
private set => this.RaiseAndSetIfChanged(ref _playerState, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates how to handle playback of data tracks
|
||||||
|
/// </summary>
|
||||||
|
public DataPlayback DataPlayback
|
||||||
|
{
|
||||||
|
get => _dataPlayback;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _dataPlayback, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -164,7 +178,8 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
private set => this.RaiseAndSetIfChanged(ref _volume, value);
|
private set => this.RaiseAndSetIfChanged(ref _volume, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool? _playing;
|
private PlayerState _playerState;
|
||||||
|
private DataPlayback _dataPlayback;
|
||||||
private bool _applyDeEmphasis;
|
private bool _applyDeEmphasis;
|
||||||
private int _volume;
|
private int _volume;
|
||||||
|
|
||||||
@@ -180,7 +195,7 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// OpticalDisc object
|
/// OpticalDisc object
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly OpticalDiscBase _opticalDisc;
|
private OpticalDiscBase _opticalDisc;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Last volume for mute toggling
|
/// Last volume for mute toggling
|
||||||
@@ -190,23 +205,29 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new Player from a given image path
|
/// Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param>
|
||||||
|
public Player(int defaultVolume)
|
||||||
|
{
|
||||||
|
Initialized = false;
|
||||||
|
_soundOutput = new SoundOutput(defaultVolume);
|
||||||
|
_soundOutput.SetDeEmphasis(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes player from a given image path
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Path to the disc image</param>
|
/// <param name="path">Path to the disc image</param>
|
||||||
/// <param name="generateMissingToc">Generate a TOC if the disc is missing one [CompactDisc only]</param>
|
/// <param name="options">Options to pass to the optical disc factory</param>
|
||||||
/// <param name="loadHiddenTracks">Load hidden tracks for playback [CompactDisc only]</param>
|
|
||||||
/// <param name="loadDataTracks">Load data tracks for playback [CompactDisc only]</param>
|
|
||||||
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
||||||
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param>
|
public void Init(string path, OpticalDiscOptions options, bool autoPlay)
|
||||||
public Player(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay, int defaultVolume)
|
|
||||||
{
|
{
|
||||||
// Set the internal state for initialization
|
// Reset initialization
|
||||||
Initialized = false;
|
Initialized = false;
|
||||||
_soundOutput = new SoundOutput();
|
|
||||||
_soundOutput.SetDeEmphasis(false);
|
|
||||||
|
|
||||||
// Initalize the disc
|
// Initalize the disc
|
||||||
_opticalDisc = OpticalDiscFactory.GenerateFromPath(path, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay);
|
_opticalDisc = OpticalDiscFactory.GenerateFromPath(path, options, autoPlay);
|
||||||
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -214,7 +235,7 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
_opticalDisc.PropertyChanged += OpticalDiscStateChanged;
|
_opticalDisc.PropertyChanged += OpticalDiscStateChanged;
|
||||||
|
|
||||||
// Initialize the sound output
|
// Initialize the sound output
|
||||||
_soundOutput.Init(_opticalDisc, autoPlay, defaultVolume);
|
_soundOutput.Init(_opticalDisc, autoPlay);
|
||||||
if(_soundOutput == null || !_soundOutput.Initialized)
|
if(_soundOutput == null || !_soundOutput.Initialized)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -240,12 +261,12 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
return;
|
return;
|
||||||
else if(_soundOutput == null)
|
else if(_soundOutput == null)
|
||||||
return;
|
return;
|
||||||
else if(_soundOutput.Playing)
|
else if(_soundOutput.PlayerState != PlayerState.Paused && _soundOutput.PlayerState != PlayerState.Stopped)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_soundOutput.Play();
|
_soundOutput.Play();
|
||||||
_opticalDisc.SetTotalIndexes();
|
_opticalDisc.SetTotalIndexes();
|
||||||
Playing = true;
|
PlayerState = PlayerState.Playing;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -257,11 +278,11 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
return;
|
return;
|
||||||
else if(_soundOutput == null)
|
else if(_soundOutput == null)
|
||||||
return;
|
return;
|
||||||
else if(!_soundOutput.Playing)
|
else if(_soundOutput.PlayerState != PlayerState.Playing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_soundOutput?.Stop();
|
_soundOutput?.Pause();
|
||||||
Playing = false;
|
PlayerState = PlayerState.Paused;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -269,10 +290,20 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void TogglePlayback()
|
public void TogglePlayback()
|
||||||
{
|
{
|
||||||
if(Playing == true)
|
switch(PlayerState)
|
||||||
Pause();
|
{
|
||||||
else
|
case PlayerState.NoDisc:
|
||||||
Play();
|
break;
|
||||||
|
case PlayerState.Stopped:
|
||||||
|
Play();
|
||||||
|
break;
|
||||||
|
case PlayerState.Paused:
|
||||||
|
Play();
|
||||||
|
break;
|
||||||
|
case PlayerState.Playing:
|
||||||
|
Pause();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -284,12 +315,12 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
return;
|
return;
|
||||||
else if(_soundOutput == null)
|
else if(_soundOutput == null)
|
||||||
return;
|
return;
|
||||||
else if(!_soundOutput.Playing)
|
else if(_soundOutput.PlayerState != PlayerState.Playing && _soundOutput.PlayerState != PlayerState.Paused)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_soundOutput?.Stop();
|
_soundOutput.Stop();
|
||||||
_opticalDisc.LoadFirstTrack();
|
_opticalDisc.LoadFirstTrack();
|
||||||
Playing = null;
|
PlayerState = PlayerState.Stopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -300,14 +331,16 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
bool? wasPlaying = Playing;
|
PlayerState wasPlaying = PlayerState;
|
||||||
if(wasPlaying == true) Pause();
|
if(wasPlaying == PlayerState.Playing)
|
||||||
|
Pause();
|
||||||
|
|
||||||
_opticalDisc.NextTrack();
|
_opticalDisc.NextTrack();
|
||||||
if(_opticalDisc is CompactDisc compactDisc)
|
if(_opticalDisc is CompactDisc compactDisc)
|
||||||
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
|
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
|
||||||
|
|
||||||
if(wasPlaying == true) Play();
|
if(wasPlaying == PlayerState.Playing)
|
||||||
|
Play();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -318,14 +351,16 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
bool? wasPlaying = Playing;
|
PlayerState wasPlaying = PlayerState;
|
||||||
if(wasPlaying == true) Pause();
|
if(wasPlaying == PlayerState.Playing)
|
||||||
|
Pause();
|
||||||
|
|
||||||
_opticalDisc.PreviousTrack();
|
_opticalDisc.PreviousTrack();
|
||||||
if(_opticalDisc is CompactDisc compactDisc)
|
if(_opticalDisc is CompactDisc compactDisc)
|
||||||
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
|
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
|
||||||
|
|
||||||
if(wasPlaying == true) Play();
|
if(wasPlaying == PlayerState.Playing)
|
||||||
|
Play();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -337,14 +372,16 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
bool? wasPlaying = Playing;
|
PlayerState wasPlaying = PlayerState;
|
||||||
if(wasPlaying == true) Pause();
|
if(wasPlaying == PlayerState.Playing)
|
||||||
|
Pause();
|
||||||
|
|
||||||
_opticalDisc.NextIndex(changeTrack);
|
_opticalDisc.NextIndex(changeTrack);
|
||||||
if(_opticalDisc is CompactDisc compactDisc)
|
if(_opticalDisc is CompactDisc compactDisc)
|
||||||
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
|
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
|
||||||
|
|
||||||
if(wasPlaying == true) Play();
|
if(wasPlaying == PlayerState.Playing)
|
||||||
|
Play();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -356,37 +393,38 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
bool? wasPlaying = Playing;
|
PlayerState wasPlaying = PlayerState;
|
||||||
if(wasPlaying == true) Pause();
|
if(wasPlaying == PlayerState.Playing)
|
||||||
|
Pause();
|
||||||
|
|
||||||
_opticalDisc.PreviousIndex(changeTrack);
|
_opticalDisc.PreviousIndex(changeTrack);
|
||||||
if(_opticalDisc is CompactDisc compactDisc)
|
if(_opticalDisc is CompactDisc compactDisc)
|
||||||
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
|
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
|
||||||
|
|
||||||
if(wasPlaying == true) Play();
|
if(wasPlaying == PlayerState.Playing)
|
||||||
|
Play();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fast-forward playback by 75 sectors, if possible
|
/// Fast-forward playback by 75 sectors
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void FastForward()
|
public void FastForward()
|
||||||
{
|
{
|
||||||
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_opticalDisc.SetCurrentSector(Math.Min(_opticalDisc.TotalSectors, _opticalDisc.CurrentSector + 75));
|
_opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector + 75);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rewind playback by 75 sectors, if possible
|
/// Rewind playback by 75 sectors
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Rewind()
|
public void Rewind()
|
||||||
{
|
{
|
||||||
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(_opticalDisc.CurrentSector >= 75)
|
_opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector - 75);
|
||||||
_opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector - 75);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -456,13 +494,13 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
#region Helpers
|
#region Helpers
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the value for loading data tracks [CompactDisc only]
|
/// Set data playback method [CompactDisc only]
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="load">True to enable loading data tracks, false otherwise</param>
|
/// <param name="dataPlayback">New playback value</param>
|
||||||
public void SetLoadDataTracks(bool load)
|
public void SetDataPlayback(DataPlayback dataPlayback)
|
||||||
{
|
{
|
||||||
if(_opticalDisc is CompactDisc compactDisc)
|
if(_opticalDisc is CompactDisc compactDisc)
|
||||||
compactDisc.LoadDataTracks = load;
|
compactDisc.DataPlayback = dataPlayback;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -508,7 +546,7 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void SoundOutputStateChanged(object sender, PropertyChangedEventArgs e)
|
private void SoundOutputStateChanged(object sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
Playing = _soundOutput.Playing;
|
PlayerState = _soundOutput.PlayerState;
|
||||||
ApplyDeEmphasis = _soundOutput.ApplyDeEmphasis;
|
ApplyDeEmphasis = _soundOutput.ApplyDeEmphasis;
|
||||||
Volume = _soundOutput.Volume;
|
Volume = _soundOutput.Volume;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,19 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicate if the output is ready to be used
|
/// Indicate if the output is ready to be used
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Initialized { get; private set; } = false;
|
public bool Initialized
|
||||||
|
{
|
||||||
|
get => _initialized;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _initialized, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicate if the output is playing
|
/// Indicates the current player state
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Playing
|
public PlayerState PlayerState
|
||||||
{
|
{
|
||||||
get => _playing;
|
get => _playerState;
|
||||||
private set => this.RaiseAndSetIfChanged(ref _playing, value);
|
private set => this.RaiseAndSetIfChanged(ref _playerState, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -54,7 +58,8 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _playing;
|
private bool _initialized;
|
||||||
|
private PlayerState _playerState;
|
||||||
private bool _applyDeEmphasis;
|
private bool _applyDeEmphasis;
|
||||||
private int _volume;
|
private int _volume;
|
||||||
|
|
||||||
@@ -102,13 +107,18 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param>
|
||||||
|
public SoundOutput(int defaultVolume = 100) => Volume = defaultVolume;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialize the output with a given image
|
/// Initialize the output with a given image
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="opticalDisc">OpticalDisc to load from</param>
|
/// <param name="opticalDisc">OpticalDisc to load from</param>
|
||||||
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
||||||
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param>
|
public void Init(OpticalDiscBase opticalDisc, bool autoPlay)
|
||||||
public void Init(OpticalDiscBase opticalDisc, bool autoPlay = false, int defaultVolume = 100)
|
|
||||||
{
|
{
|
||||||
// If we have an unusable disc, just return
|
// If we have an unusable disc, just return
|
||||||
if(opticalDisc == null || !opticalDisc.Initialized)
|
if(opticalDisc == null || !opticalDisc.Initialized)
|
||||||
@@ -117,9 +127,6 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
// Save a reference to the disc
|
// Save a reference to the disc
|
||||||
_opticalDisc = opticalDisc;
|
_opticalDisc = opticalDisc;
|
||||||
|
|
||||||
// Set the initial playback volume
|
|
||||||
Volume = defaultVolume;
|
|
||||||
|
|
||||||
// Enable de-emphasis for CDs, if necessary
|
// Enable de-emphasis for CDs, if necessary
|
||||||
if(opticalDisc is CompactDisc compactDisc)
|
if(opticalDisc is CompactDisc compactDisc)
|
||||||
ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
|
ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
|
||||||
@@ -136,6 +143,7 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
|
|
||||||
// Mark the output as ready
|
// Mark the output as ready
|
||||||
Initialized = true;
|
Initialized = true;
|
||||||
|
PlayerState = PlayerState.Stopped;
|
||||||
|
|
||||||
// Begin loading data
|
// Begin loading data
|
||||||
_source.Start();
|
_source.Start();
|
||||||
@@ -163,57 +171,14 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
// Determine how many sectors we can read
|
// Determine how many sectors we can read
|
||||||
DetermineReadAmount(count, out ulong sectorsToRead, out ulong zeroSectorsAmount);
|
DetermineReadAmount(count, out ulong sectorsToRead, out ulong zeroSectorsAmount);
|
||||||
|
|
||||||
// Create padding data for overreads
|
// Get data to return
|
||||||
byte[] zeroSectors = new byte[(int)zeroSectorsAmount * _opticalDisc.BytesPerSector];
|
byte[] audioDataSegment = ReadData(count, sectorsToRead, zeroSectorsAmount);
|
||||||
byte[] audioData;
|
if(audioDataSegment == null)
|
||||||
|
|
||||||
// Attempt to read the required number of sectors
|
|
||||||
var readSectorTask = Task.Run(() =>
|
|
||||||
{
|
|
||||||
lock(_readingImage)
|
|
||||||
{
|
|
||||||
for(int i = 0; i < 4; i++)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return _opticalDisc.ReadSectors((uint)sectorsToRead).Concat(zeroSectors).ToArray();
|
|
||||||
}
|
|
||||||
catch(ArgumentOutOfRangeException)
|
|
||||||
{
|
|
||||||
_opticalDisc.LoadFirstTrack();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return zeroSectors;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait 100ms at longest for the read to occur
|
|
||||||
if(readSectorTask.Wait(TimeSpan.FromMilliseconds(100)))
|
|
||||||
{
|
|
||||||
audioData = readSectorTask.Result;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
Array.Clear(buffer, offset, count);
|
Array.Clear(buffer, offset, count);
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load only the requested audio segment
|
|
||||||
byte[] audioDataSegment = new byte[count];
|
|
||||||
int copyAmount = Math.Min(count, audioData.Length - _currentSectorReadPosition);
|
|
||||||
if(Math.Max(0, copyAmount) == 0)
|
|
||||||
{
|
|
||||||
Array.Clear(buffer, offset, count);
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
Array.Copy(audioData, _currentSectorReadPosition, audioDataSegment, 0, copyAmount);
|
|
||||||
|
|
||||||
// Apply de-emphasis filtering, only if enabled
|
|
||||||
if(ApplyDeEmphasis)
|
|
||||||
ProcessDeEmphasis(audioDataSegment);
|
|
||||||
|
|
||||||
// Write out the audio data to the buffer
|
// Write out the audio data to the buffer
|
||||||
Array.Copy(audioDataSegment, 0, buffer, offset, count);
|
Array.Copy(audioDataSegment, 0, buffer, offset, count);
|
||||||
|
|
||||||
@@ -221,6 +186,7 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
_currentSectorReadPosition += count;
|
_currentSectorReadPosition += count;
|
||||||
if(_currentSectorReadPosition >= _opticalDisc.BytesPerSector)
|
if(_currentSectorReadPosition >= _opticalDisc.BytesPerSector)
|
||||||
{
|
{
|
||||||
|
int currentTrack = _opticalDisc.CurrentTrackNumber;
|
||||||
_opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector + (ulong)(_currentSectorReadPosition / _opticalDisc.BytesPerSector));
|
_opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector + (ulong)(_currentSectorReadPosition / _opticalDisc.BytesPerSector));
|
||||||
_currentSectorReadPosition %= _opticalDisc.BytesPerSector;
|
_currentSectorReadPosition %= _opticalDisc.BytesPerSector;
|
||||||
}
|
}
|
||||||
@@ -235,10 +201,10 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Play()
|
public void Play()
|
||||||
{
|
{
|
||||||
if (_soundOut.PlaybackState != PlaybackState.Playing)
|
if(_soundOut.PlaybackState != PlaybackState.Playing)
|
||||||
_soundOut.Play();
|
_soundOut.Play();
|
||||||
|
|
||||||
Playing = _soundOut.PlaybackState == PlaybackState.Playing;
|
PlayerState = PlayerState.Playing;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -249,7 +215,7 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
if(_soundOut.PlaybackState != PlaybackState.Paused)
|
if(_soundOut.PlaybackState != PlaybackState.Paused)
|
||||||
_soundOut.Pause();
|
_soundOut.Pause();
|
||||||
|
|
||||||
Playing = _soundOut.PlaybackState == PlaybackState.Playing;
|
PlayerState = PlayerState.Paused;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -260,7 +226,7 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
if(_soundOut.PlaybackState != PlaybackState.Stopped)
|
if(_soundOut.PlaybackState != PlaybackState.Stopped)
|
||||||
_soundOut.Stop();
|
_soundOut.Stop();
|
||||||
|
|
||||||
Playing = _soundOut.PlaybackState == PlaybackState.Playing;
|
PlayerState = PlayerState.Stopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -270,7 +236,7 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set de-emphasis status
|
/// Set de-emphasis status
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apply"></param>
|
/// <param name="apply">New de-emphasis status</param>
|
||||||
public void SetDeEmphasis(bool apply) => ApplyDeEmphasis = apply;
|
public void SetDeEmphasis(bool apply) => ApplyDeEmphasis = apply;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -287,30 +253,19 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
/// <param name="zeroSectorsAmount">Number of zeroed sectors to concatenate</param>
|
/// <param name="zeroSectorsAmount">Number of zeroed sectors to concatenate</param>
|
||||||
private void DetermineReadAmount(int count, out ulong sectorsToRead, out ulong zeroSectorsAmount)
|
private void DetermineReadAmount(int count, out ulong sectorsToRead, out ulong zeroSectorsAmount)
|
||||||
{
|
{
|
||||||
do
|
// Attempt to read 5 more sectors than requested
|
||||||
|
sectorsToRead = ((ulong)count / (ulong)_opticalDisc.BytesPerSector) + 5;
|
||||||
|
zeroSectorsAmount = 0;
|
||||||
|
|
||||||
|
// Avoid overreads by padding with 0-byte data at the end
|
||||||
|
if(_opticalDisc.CurrentSector + sectorsToRead > _opticalDisc.TotalSectors)
|
||||||
{
|
{
|
||||||
// Attempt to read 2 more sectors than requested
|
ulong oldSectorsToRead = sectorsToRead;
|
||||||
sectorsToRead = ((ulong)count / (ulong)_opticalDisc.BytesPerSector) + 2;
|
sectorsToRead = _opticalDisc.TotalSectors - _opticalDisc.CurrentSector;
|
||||||
zeroSectorsAmount = 0;
|
|
||||||
|
|
||||||
// Avoid overreads by padding with 0-byte data at the end
|
int tempZeroSectorCount = (int)(oldSectorsToRead - sectorsToRead);
|
||||||
if(_opticalDisc.CurrentSector + sectorsToRead > _opticalDisc.TotalSectors)
|
zeroSectorsAmount = (ulong)(tempZeroSectorCount < 0 ? 0 : tempZeroSectorCount);
|
||||||
{
|
}
|
||||||
ulong oldSectorsToRead = sectorsToRead;
|
|
||||||
sectorsToRead = _opticalDisc.TotalSectors - _opticalDisc.CurrentSector;
|
|
||||||
|
|
||||||
int tempZeroSectorCount = (int)(oldSectorsToRead - sectorsToRead);
|
|
||||||
zeroSectorsAmount = (ulong)(tempZeroSectorCount < 0 ? 0 : tempZeroSectorCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're reading past the last sector of the disc, wrap around
|
|
||||||
// TODO: Have past-end reads looping back controlled by a flag instead (Repeat? Repeat All?)
|
|
||||||
if(sectorsToRead <= 0)
|
|
||||||
{
|
|
||||||
_opticalDisc.LoadFirstTrack();
|
|
||||||
_currentSectorReadPosition = 0;
|
|
||||||
}
|
|
||||||
} while(sectorsToRead <= 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -333,6 +288,58 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
ByteConverter.FromFloats16Bit(floatAudioData, audioData);
|
ByteConverter.FromFloats16Bit(floatAudioData, audioData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read the requested amount of data from an input
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="count">Number of bytes to load</param>
|
||||||
|
/// <param name="sectorsToRead">Number of sectors to read</param>
|
||||||
|
/// <param name="zeroSectorsAmount">Number of zeroed sectors to concatenate</param>
|
||||||
|
/// <returns>The requested amount of data, if possible</returns>
|
||||||
|
private byte[] ReadData(int count, ulong sectorsToRead, ulong zeroSectorsAmount)
|
||||||
|
{
|
||||||
|
// Create padding data for overreads
|
||||||
|
byte[] zeroSectors = new byte[(int)zeroSectorsAmount * _opticalDisc.BytesPerSector];
|
||||||
|
byte[] audioData;
|
||||||
|
|
||||||
|
// Attempt to read the required number of sectors
|
||||||
|
var readSectorTask = Task.Run(() =>
|
||||||
|
{
|
||||||
|
lock(_readingImage)
|
||||||
|
{
|
||||||
|
for(int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _opticalDisc.ReadSectors((uint)sectorsToRead).Concat(zeroSectors).ToArray();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
return zeroSectors;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait 100ms at longest for the read to occur
|
||||||
|
if(readSectorTask.Wait(TimeSpan.FromMilliseconds(100)))
|
||||||
|
audioData = readSectorTask.Result;
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Load only the requested audio segment
|
||||||
|
byte[] audioDataSegment = new byte[count];
|
||||||
|
int copyAmount = Math.Min(count, audioData.Length - _currentSectorReadPosition);
|
||||||
|
if(Math.Max(0, copyAmount) == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
Array.Copy(audioData, _currentSectorReadPosition, audioDataSegment, 0, copyAmount);
|
||||||
|
|
||||||
|
// Apply de-emphasis filtering, only if enabled
|
||||||
|
if(ApplyDeEmphasis)
|
||||||
|
ProcessDeEmphasis(audioDataSegment);
|
||||||
|
|
||||||
|
return audioDataSegment;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets or resets the de-emphasis filters
|
/// Sets or resets the de-emphasis filters
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -369,4 +376,4 @@ namespace RedBookPlayer.Models.Hardware
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||