This commit is contained in:
		
							parent
							
								
									2a8166a0c5
								
							
						
					
					
						commit
						d881ed69e2
					
				
					 33 changed files with 580 additions and 640 deletions
				
			
		| 
						 | 
					@ -2,85 +2,80 @@
 | 
				
			||||||
using Plpext.Core.Interfaces;
 | 
					using Plpext.Core.Interfaces;
 | 
				
			||||||
using Plpext.Core.Models;
 | 
					using Plpext.Core.Models;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Plpext.Core.AudioConverter
 | 
					namespace Plpext.Core.AudioConverter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class MP3AudioConverter : IAudioConverter
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public class MP3AudioConverter : IAudioConverter
 | 
					    private readonly IMP3Parser _parser;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public MP3AudioConverter(IMP3Parser parser)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private readonly IMP3Parser _parser;
 | 
					        _parser = parser;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    public async Task<AudioFile> ConvertAudioAsync(ReadOnlyMemory<byte> mp3Input, CancellationToken cancellationToken)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        var baseFile = await _parser.ParseIntoMP3(mp3Input, cancellationToken);
 | 
				
			||||||
 | 
					        byte[] pcmData = null!;
 | 
				
			||||||
 | 
					        await using var mp3Stream = new MP3Stream(new MemoryStream(baseFile.Data.ToArray()));
 | 
				
			||||||
 | 
					        await using var pcmStream = new MemoryStream();
 | 
				
			||||||
 | 
					        var buffer = new byte[4096];
 | 
				
			||||||
 | 
					        int bytesReturned = 1;
 | 
				
			||||||
 | 
					        int totalBytesRead = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public MP3AudioConverter(IMP3Parser parser)
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _parser = parser;
 | 
					            while (bytesReturned > 0)
 | 
				
			||||||
        }
 | 
					            {
 | 
				
			||||||
        public async Task<AudioFile> ConvertAudioAsync(ReadOnlyMemory<byte> file, CancellationToken cancellationToken)
 | 
					                try
 | 
				
			||||||
        {
 | 
					                {
 | 
				
			||||||
            var baseFile = await _parser.ParseIntoMP3(file, cancellationToken);
 | 
					                    bytesReturned = await mp3Stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
 | 
				
			||||||
            byte[] pcmData = null!;
 | 
					                }
 | 
				
			||||||
            using var mp3Stream = new MP3Stream(new MemoryStream(baseFile.Data.ToArray()));
 | 
					                catch (IndexOutOfRangeException)
 | 
				
			||||||
            using var pcmStream = new MemoryStream();
 | 
					                {
 | 
				
			||||||
            var buffer = new byte[4096];
 | 
					                    //File reached a corrupted/non-compliant portion.
 | 
				
			||||||
            int bytesReturned = 1;
 | 
					                    break;
 | 
				
			||||||
            int totalBytesRead = 0;
 | 
					                }
 | 
				
			||||||
 | 
					                catch (NullReferenceException)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    //File reached a corrupted/non-compliant portion that caused MP3Sharp to throw a NRE.
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                totalBytesRead += bytesReturned;
 | 
				
			||||||
 | 
					                await pcmStream.WriteAsync(buffer, 0, bytesReturned, cancellationToken);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            try
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                while (bytesReturned > 0)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    try
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        bytesReturned = await mp3Stream.ReadAsync(buffer, 0, buffer.Length);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    catch (IndexOutOfRangeException ex)
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        Console.WriteLine($"{ex.Message}");
 | 
					 | 
				
			||||||
                        Console.WriteLine($"File reached a corrupted/non-compliant portion. Total bytes read: {totalBytesRead} | File: {baseFile.Name}");
 | 
					 | 
				
			||||||
                        break;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    catch (NullReferenceException ex)
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        Console.WriteLine($"{ex.Message}");
 | 
					 | 
				
			||||||
                        Console.WriteLine($"File reached a corrupted/non-compliant portion. Total bytes read: {totalBytesRead} | File: {baseFile.Name}");
 | 
					 | 
				
			||||||
                        break;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    totalBytesRead += bytesReturned;
 | 
					 | 
				
			||||||
                    await pcmStream.WriteAsync(buffer, 0, bytesReturned);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            catch (Exception ex)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                Console.WriteLine("Exception details:");
 | 
					 | 
				
			||||||
                Console.WriteLine($"Message: {ex.Message}");
 | 
					 | 
				
			||||||
                Console.WriteLine($"StackTrace: {ex.StackTrace}");
 | 
					 | 
				
			||||||
                if (ex.InnerException != null)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    Console.WriteLine($"InnerException: {ex.InnerException.Message}");
 | 
					 | 
				
			||||||
                    Console.WriteLine($"InnerException StackTrace: {ex.InnerException.StackTrace}");
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            pcmData = ResampleToMono(pcmStream.ToArray());
 | 
					            pcmData = ResampleToMono(pcmStream.ToArray());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var audioDuration = (double)(pcmData.Length / 2) / mp3Stream.Frequency;
 | 
					            var audioDuration = (double)(pcmData.Length / 2) / mp3Stream.Frequency;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return new AudioFile() { Name = baseFile.Name, MP3Data = file, Data = pcmData, Duration = TimeSpan.FromSeconds(audioDuration), Format = AudioFormat.Mono16, Frequency = mp3Stream.Frequency };
 | 
					            return new AudioFile() { Name = baseFile.Name, MP3Data = mp3Input, Data = pcmData, Duration = TimeSpan.FromSeconds(audioDuration), Format = AudioFormat.Mono16, Frequency = mp3Stream.Frequency };
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        catch (Exception)
 | 
				
			||||||
        private static byte[] ResampleToMono(Span<byte> data)
 | 
					 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            byte[] newData = new byte[data.Length / 2];
 | 
					            //An exception here would be something new.
 | 
				
			||||||
 | 
					 | 
				
			||||||
            for (int i = 0; i < data.Length / 4; ++i)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                int HI = 1;
 | 
					 | 
				
			||||||
                int LO = 0;
 | 
					 | 
				
			||||||
                short left = (short)((data[i * 4 + HI] << 8) | (data[i * 4 + LO] & 0xff));
 | 
					 | 
				
			||||||
                short right = (short)((data[i * 4 + 2 + HI] << 8) | (data[i * 4 + 2 + LO] & 0xff));
 | 
					 | 
				
			||||||
                int avg = (left + right) / 2;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                newData[i * 2 + HI] = (byte)((avg >> 8) & 0xff);
 | 
					 | 
				
			||||||
                newData[i * 2 + LO] = (byte)((avg & 0xff));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return newData;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        return new AudioFile() { Name = $"${baseFile.Name} failed extraction", Data = ReadOnlyMemory<byte>.Empty, MP3Data = ReadOnlyMemory<byte>.Empty, Duration = TimeSpan.FromSeconds(0), Format = AudioFormat.Unknown, Frequency = 0 };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* This is pretty cursed, but it works.
 | 
				
			||||||
 | 
					       I don't remember where I got it from the first time, though. I think LLMs weren't a thing yet.
 | 
				
			||||||
 | 
					    */
 | 
				
			||||||
 | 
					    private static byte[] ResampleToMono(Span<byte> data)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        byte[] newData = new byte[data.Length / 2];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (int i = 0; i < data.Length / 4; ++i)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            int HI = 1;
 | 
				
			||||||
 | 
					            int LO = 0;
 | 
				
			||||||
 | 
					            short left = (short)((data[i * 4 + HI] << 8) | (data[i * 4 + LO] & 0xff));
 | 
				
			||||||
 | 
					            short right = (short)((data[i * 4 + 2 + HI] << 8) | (data[i * 4 + 2 + LO] & 0xff));
 | 
				
			||||||
 | 
					            int avg = (left + right) / 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            newData[i * 2 + HI] = (byte)((avg >> 8) & 0xff);
 | 
				
			||||||
 | 
					            newData[i * 2 + LO] = (byte)((avg & 0xff));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return newData;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,6 @@
 | 
				
			||||||
using OpenTK.Audio.OpenAL;
 | 
					using OpenTK.Audio.OpenAL;
 | 
				
			||||||
using Plpext.Core.Interfaces;
 | 
					using Plpext.Core.Interfaces;
 | 
				
			||||||
using Plpext.Core.Models;
 | 
					using Plpext.Core.Models;
 | 
				
			||||||
using System;
 | 
					 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Diagnostics;
 | 
					 | 
				
			||||||
using System.Linq;
 | 
					 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
using System.Xml.Linq;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Plpext.Core.AudioPlayer;
 | 
					namespace Plpext.Core.AudioPlayer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,13 +34,12 @@ public sealed class AudioPlayer : IAudioPlayer, IDisposable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            AL.BufferData(bufferId, ALFormat.Mono16, input.Data.Span, input.Frequency);
 | 
					            AL.BufferData(bufferId, ALFormat.Mono16, input.Data.Span, input.Frequency);
 | 
				
			||||||
            AL.Source(sourceId, ALSourcei.Buffer, bufferId);
 | 
					            AL.Source(sourceId, ALSourcei.Buffer, bufferId);
 | 
				
			||||||
            if(autoStart)
 | 
					            if (autoStart)
 | 
				
			||||||
                return await Start();
 | 
					                return await Start();
 | 
				
			||||||
            return true;
 | 
					            return true;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (Exception ex)
 | 
					        catch (Exception)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Console.WriteLine($"init error: {ex}");
 | 
					 | 
				
			||||||
            await CleanupPlaybackResources();
 | 
					            await CleanupPlaybackResources();
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -63,7 +55,6 @@ public sealed class AudioPlayer : IAudioPlayer, IDisposable
 | 
				
			||||||
            while (!cancellationToken.IsCancellationRequested)
 | 
					            while (!cancellationToken.IsCancellationRequested)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                var sourceState = (ALSourceState)AL.GetSource(_currentSourceId.Value, ALGetSourcei.SourceState);
 | 
					                var sourceState = (ALSourceState)AL.GetSource(_currentSourceId.Value, ALGetSourcei.SourceState);
 | 
				
			||||||
                Console.WriteLine($"source state: {sourceState}");
 | 
					 | 
				
			||||||
                if (sourceState == ALSourceState.Stopped)
 | 
					                if (sourceState == ALSourceState.Stopped)
 | 
				
			||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
                if (State != PlaybackState.Paused)
 | 
					                if (State != PlaybackState.Paused)
 | 
				
			||||||
| 
						 | 
					@ -73,8 +64,6 @@ public sealed class AudioPlayer : IAudioPlayer, IDisposable
 | 
				
			||||||
                        ((double)(_audioFile.Data.Length - (_audioFile.Data.Length - bytesPlayed)) / 2) / _audioFile.Frequency
 | 
					                        ((double)(_audioFile.Data.Length - (_audioFile.Data.Length - bytesPlayed)) / 2) / _audioFile.Frequency
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    Console.WriteLine($"Progress: {currentPosition.TotalSeconds:F2}s / {_audioFile.Duration.TotalSeconds:F2}s");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    OnProgressUpdated?.Invoke(this, new()
 | 
					                    OnProgressUpdated?.Invoke(this, new()
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        CurrentPosition = currentPosition,
 | 
					                        CurrentPosition = currentPosition,
 | 
				
			||||||
| 
						 | 
					@ -86,15 +75,14 @@ public sealed class AudioPlayer : IAudioPlayer, IDisposable
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (OperationCanceledException)
 | 
					        catch (OperationCanceledException)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Console.WriteLine("Playback cancelled");
 | 
					            //An operation cancelled here is expected behavior.
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (Exception ex)
 | 
					        catch (Exception)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Console.WriteLine($"Monitor error: {ex}");
 | 
					            //There is little to be done if we get any other exception during monitoring. Finish up playback monitoring.
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        finally
 | 
					        finally
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Console.WriteLine("Monitor task ending");
 | 
					 | 
				
			||||||
            OnProgressUpdated?.Invoke(this, new()
 | 
					            OnProgressUpdated?.Invoke(this, new()
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                CurrentPosition = _audioFile.Duration,
 | 
					                CurrentPosition = _audioFile.Duration,
 | 
				
			||||||
| 
						 | 
					@ -108,9 +96,9 @@ public sealed class AudioPlayer : IAudioPlayer, IDisposable
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        lock (_lock)
 | 
					        lock (_lock)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            //If we try and start/resume playback without a proper source id or audio file, we can't go on.
 | 
				
			||||||
            if (_currentSourceId == null || _audioFile == null)
 | 
					            if (_currentSourceId == null || _audioFile == null)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                Console.WriteLine("Cannot start - no source or audio file");
 | 
					 | 
				
			||||||
                return Task.FromResult(false);
 | 
					                return Task.FromResult(false);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -137,16 +125,13 @@ public sealed class AudioPlayer : IAudioPlayer, IDisposable
 | 
				
			||||||
                State = PlaybackState.Playing;
 | 
					                State = PlaybackState.Playing;
 | 
				
			||||||
                OnPlaybackStarted?.Invoke(this, new PlaybackStartedEventArgs { AudioFile = _audioFile });
 | 
					                OnPlaybackStarted?.Invoke(this, new PlaybackStartedEventArgs { AudioFile = _audioFile });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Console.WriteLine($"Rewind");
 | 
					 | 
				
			||||||
                AL.SourceRewind(_currentSourceId.Value);
 | 
					                AL.SourceRewind(_currentSourceId.Value);
 | 
				
			||||||
                Console.WriteLine($"Play");
 | 
					 | 
				
			||||||
                AL.SourcePlay(_currentSourceId.Value);
 | 
					                AL.SourcePlay(_currentSourceId.Value);
 | 
				
			||||||
                _playbackTask = Task.Run(() => MonitorPlaybackAsync(_playbackCts.Token));
 | 
					                _playbackTask = Task.Run(() => MonitorPlaybackAsync(_playbackCts.Token));
 | 
				
			||||||
                return Task.FromResult(true);
 | 
					                return Task.FromResult(true);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            catch (Exception ex)
 | 
					            catch (Exception)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                Console.WriteLine($"Start error: {ex}");
 | 
					 | 
				
			||||||
                return Task.FromResult(false);
 | 
					                return Task.FromResult(false);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -177,7 +162,6 @@ public sealed class AudioPlayer : IAudioPlayer, IDisposable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public void Stop()
 | 
					    public void Stop()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        Console.WriteLine($"STOP called");
 | 
					 | 
				
			||||||
        lock (_lock)
 | 
					        lock (_lock)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _playbackCts?.Cancel();
 | 
					            _playbackCts?.Cancel();
 | 
				
			||||||
| 
						 | 
					@ -190,7 +174,8 @@ public sealed class AudioPlayer : IAudioPlayer, IDisposable
 | 
				
			||||||
            _playbackCts?.Cancel();
 | 
					            _playbackCts?.Cancel();
 | 
				
			||||||
            State = PlaybackState.Stopped;
 | 
					            State = PlaybackState.Stopped;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        OnPlaybackStopped?.Invoke(this, new() { AudioFile = _audioFile });
 | 
					        if(_audioFile is not null)
 | 
				
			||||||
 | 
					            OnPlaybackStopped?.Invoke(this, new() { AudioFile = _audioFile });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private Task CleanupPlaybackResources()
 | 
					    private Task CleanupPlaybackResources()
 | 
				
			||||||
| 
						 | 
					@ -209,9 +194,9 @@ public sealed class AudioPlayer : IAudioPlayer, IDisposable
 | 
				
			||||||
                _currentBufferId = null;
 | 
					                _currentBufferId = null;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (Exception ex)
 | 
					        catch (Exception)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Console.WriteLine($"Error during cleanup: {ex}");
 | 
					            //Things might be pretty broken at this point if this exception pops.
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return Task.CompletedTask;
 | 
					        return Task.CompletedTask;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,45 +1,41 @@
 | 
				
			||||||
using Plpext.Core.Interfaces;
 | 
					using Plpext.Core.Interfaces;
 | 
				
			||||||
using Plpext.Core.Models;
 | 
					using Plpext.Core.Models;
 | 
				
			||||||
using System;
 | 
					 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Diagnostics;
 | 
					using System.Diagnostics;
 | 
				
			||||||
using System.Linq;
 | 
					 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Plpext.Core.FileStorage
 | 
					namespace Plpext.Core.FileStorage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class DiskFileStorage : IFileStorage
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public class DiskFileStorage : IFileStorage
 | 
					    public async Task SaveFilesAsync(IEnumerable<MP3File> files, string targetPath)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        public async Task SaveFilesAsync(IEnumerable<MP3File> files, string targetPath)
 | 
					        HashSet<string> names = [];
 | 
				
			||||||
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            HashSet<string> names = new HashSet<string>();
 | 
					            if (!Directory.Exists(targetPath))
 | 
				
			||||||
 | 
					                Directory.CreateDirectory(targetPath);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        catch (Exception)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach (var file in files)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var finalFileName = TryAddName(names, string.Join("_", file.Name.Split(Path.GetInvalidFileNameChars())));
 | 
				
			||||||
            try
 | 
					            try
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                if (!Directory.Exists(targetPath))
 | 
					                await File.Create(Path.Combine(targetPath, $"{finalFileName}.mp3")).WriteAsync(file.Data);
 | 
				
			||||||
                    Directory.CreateDirectory(targetPath);
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            catch (Exception e)
 | 
					            catch (Exception)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                Debug.WriteLine(e);
 | 
					                //We can proceed with the next ones.
 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					 | 
				
			||||||
            foreach (var file in files)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                var finalFileName = TryAddName(names, string.Join("_", file.Name.Split(Path.GetInvalidFileNameChars())));
 | 
					 | 
				
			||||||
                try
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    await File.Create(Path.Combine(targetPath, $"{finalFileName}.mp3")).WriteAsync(file.Data);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                catch (Exception e) { Debug.WriteLine(e); }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        private static string TryAddName(HashSet<string> names, string newName)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            if (!names.Add(newName))
 | 
					 | 
				
			||||||
                return TryAddName(names, newName + "_");
 | 
					 | 
				
			||||||
            return newName;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    private static string TryAddName(HashSet<string> names, string newName)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (!names.Add(newName))
 | 
				
			||||||
 | 
					            return TryAddName(names, newName + "_");
 | 
				
			||||||
 | 
					        return newName;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,4 @@
 | 
				
			||||||
using Plpext.Core.Models;
 | 
					using Plpext.Core.Models;
 | 
				
			||||||
using System;
 | 
					 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Linq;
 | 
					 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Plpext.Core.Interfaces;
 | 
					namespace Plpext.Core.Interfaces;
 | 
				
			||||||
public interface IAudioConverter
 | 
					public interface IAudioConverter
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,4 @@
 | 
				
			||||||
using Plpext.Core.Models;
 | 
					using Plpext.Core.Models;
 | 
				
			||||||
using System;
 | 
					 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Linq;
 | 
					 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Plpext.Core.Interfaces;
 | 
					namespace Plpext.Core.Interfaces;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,8 @@
 | 
				
			||||||
using Plpext.Core.Models;
 | 
					using Plpext.Core.Models;
 | 
				
			||||||
using System;
 | 
					 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Linq;
 | 
					 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Plpext.Core.Interfaces
 | 
					namespace Plpext.Core.Interfaces;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public interface IFileStorage
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public interface IFileStorage
 | 
					    Task SaveFilesAsync(IEnumerable<MP3File> files, string targetPath);
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        Task SaveFilesAsync(IEnumerable<MP3File> files, string targetPath);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,8 @@
 | 
				
			||||||
using Plpext.Core.Models;
 | 
					using Plpext.Core.Models;
 | 
				
			||||||
using System;
 | 
					 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Linq;
 | 
					 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Plpext.Core.Interfaces
 | 
					namespace Plpext.Core.Interfaces;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public interface IMP3Parser
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public interface IMP3Parser
 | 
					    Task<MP3File> ParseIntoMP3(ReadOnlyMemory<byte> data, CancellationToken cancellationToken);
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        Task<MP3File> ParseIntoMP3(ReadOnlyMemory<byte> data, CancellationToken cancellationToken);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,4 @@
 | 
				
			||||||
using System;
 | 
					namespace Plpext.Core.Interfaces;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Linq;
 | 
					 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Plpext.Core.Interfaces;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
public interface IPackExtractor
 | 
					public interface IPackExtractor
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,21 +1,15 @@
 | 
				
			||||||
using MP3Sharp;
 | 
					using Plpext.Core.Interfaces;
 | 
				
			||||||
using Plpext.Core.Interfaces;
 | 
					 | 
				
			||||||
using Plpext.Core.Models;
 | 
					using Plpext.Core.Models;
 | 
				
			||||||
using System;
 | 
					 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Linq;
 | 
					 | 
				
			||||||
using System.Text;
 | 
					using System.Text;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Plpext.Core.MP3Parser
 | 
					namespace Plpext.Core.MP3Parser;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class MP3Parser : IMP3Parser
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public class MP3Parser : IMP3Parser
 | 
					    public Task<MP3File> ParseIntoMP3(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        public Task<MP3File> ParseIntoMP3(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
 | 
					        var fileName = Encoding.Latin1.GetString(data.Slice(start: 24, length: 260).Span).Split('\0')[0].Normalize().Trim();
 | 
				
			||||||
        {
 | 
					        var fileData = data[284..];
 | 
				
			||||||
            var fileName = Encoding.Latin1.GetString(data.Slice(start: 24, length: 260).Span).Split('\0')[0].Normalize().Trim();
 | 
					        return Task.FromResult(new MP3File() { Name = fileName, Data = fileData });
 | 
				
			||||||
            var fileData = data[284..];
 | 
					 | 
				
			||||||
            return Task.FromResult(new MP3File() { Name = fileName, Data = fileData });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,18 +1,11 @@
 | 
				
			||||||
using System;
 | 
					namespace Plpext.Core.Models;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Linq;
 | 
					 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Plpext.Core.Models
 | 
					public record AudioFile
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public record AudioFile
 | 
					    public required string Name { get; init; }
 | 
				
			||||||
    {
 | 
					    public ReadOnlyMemory<byte> MP3Data { get; init; } 
 | 
				
			||||||
        public required string Name { get; init; }
 | 
					    public ReadOnlyMemory<byte> Data { get; init; }
 | 
				
			||||||
        public ReadOnlyMemory<byte> MP3Data { get; init; } 
 | 
					    public TimeSpan Duration { get; init; }
 | 
				
			||||||
        public ReadOnlyMemory<byte> Data { get; init; }
 | 
					    public AudioFormat Format { get; init; }
 | 
				
			||||||
        public TimeSpan Duration { get; init; }
 | 
					    public int Frequency { get; init; }
 | 
				
			||||||
        public AudioFormat Format { get; init; }
 | 
					 | 
				
			||||||
        public int Frequency { get; init; }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,4 @@
 | 
				
			||||||
using System;
 | 
					namespace Plpext.Core.Models;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Linq;
 | 
					 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Plpext.Core.Models;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
public enum AudioFormat
 | 
					public enum AudioFormat
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,17 +1,11 @@
 | 
				
			||||||
using System;
 | 
					namespace Plpext.Core.Models;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Linq;
 | 
					 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Plpext.Core.Models;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class PlaybackStoppedEventArgs : EventArgs
 | 
					public class PlaybackStoppedEventArgs : EventArgs
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public AudioFile AudioFile { get; init; }
 | 
					    public required AudioFile AudioFile { get; init; }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class PlaybackStartedEventArgs : EventArgs
 | 
					public class PlaybackStartedEventArgs : EventArgs
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public AudioFile AudioFile { get; init; }
 | 
					    public required AudioFile AudioFile { get; init; }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,7 @@
 | 
				
			||||||
using System;
 | 
					namespace Plpext.Core.Models;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Linq;
 | 
					 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Plpext.Core.Models
 | 
					public record MP3File
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public record MP3File
 | 
					    public required string Name { get; init; }
 | 
				
			||||||
    {
 | 
					    public ReadOnlyMemory<byte> Data { get; init; }
 | 
				
			||||||
        public required string Name { get; init; }
 | 
					 | 
				
			||||||
        public ReadOnlyMemory<byte> Data { get; init; }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,8 @@
 | 
				
			||||||
using System;
 | 
					namespace Plpext.Core.Models;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Linq;
 | 
					 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Plpext.Core.Models
 | 
					public record PlaybackProgress
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public record PlaybackProgress
 | 
					    public TimeSpan CurrentPosition { get; init; }
 | 
				
			||||||
    {
 | 
					    public TimeSpan TotalDuration { get; init; }
 | 
				
			||||||
        public TimeSpan CurrentPosition { get; init; }
 | 
					    public double ProgressPercentage => TotalDuration.TotalSeconds > 0 ? (CurrentPosition.TotalSeconds / TotalDuration.TotalSeconds) * 100 : 0;
 | 
				
			||||||
        public TimeSpan TotalDuration { get; init; }
 | 
					 | 
				
			||||||
        public double ProgressPercentage => TotalDuration.TotalSeconds > 0 ? (CurrentPosition.TotalSeconds / TotalDuration.TotalSeconds) * 100 : 0;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,5 @@
 | 
				
			||||||
using Plpext.Core.Interfaces;
 | 
					using Plpext.Core.Interfaces;
 | 
				
			||||||
using System;
 | 
					 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Diagnostics;
 | 
					using System.Diagnostics;
 | 
				
			||||||
using System.Linq;
 | 
					 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Plpext.Core.PackExtractor
 | 
					namespace Plpext.Core.PackExtractor
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					@ -13,24 +8,16 @@ namespace Plpext.Core.PackExtractor
 | 
				
			||||||
        private static readonly byte[] filePattern = { 0x53, 0x4E, 0x44, 0x55, 0x00 };
 | 
					        private static readonly byte[] filePattern = { 0x53, 0x4E, 0x44, 0x55, 0x00 };
 | 
				
			||||||
        public async Task<IEnumerable<ReadOnlyMemory<byte>>> GetFileListAsync(string filePath, CancellationToken cancellationToken)
 | 
					        public async Task<IEnumerable<ReadOnlyMemory<byte>>> GetFileListAsync(string filePath, CancellationToken cancellationToken)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            try
 | 
					            ReadOnlyMemory<byte> file = await File.ReadAllBytesAsync(filePath, cancellationToken);
 | 
				
			||||||
            {
 | 
					            var fileIndexes = FindFileIndexes(file.Span);
 | 
				
			||||||
                ReadOnlyMemory<byte> file = await File.ReadAllBytesAsync(filePath, cancellationToken);
 | 
					 | 
				
			||||||
                var fileIndexes = FindFileIndexes(file.Span);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                var result = new List<ReadOnlyMemory<byte>>();
 | 
					            var result = new List<ReadOnlyMemory<byte>>();
 | 
				
			||||||
                for(int i = 0; i < fileIndexes.Count - 1; ++i)
 | 
					            for (int i = 0; i < fileIndexes.Count - 1; ++i)
 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    var nextFile = file.Slice(start: fileIndexes[i], length: fileIndexes[i + 1] - fileIndexes[i]);
 | 
					 | 
				
			||||||
                    result.Add(nextFile);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                return result;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            catch(Exception e)
 | 
					 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                Debug.WriteLine(e);
 | 
					                var nextFile = file.Slice(start: fileIndexes[i], length: fileIndexes[i + 1] - fileIndexes[i]);
 | 
				
			||||||
                throw;
 | 
					                result.Add(nextFile);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            return result;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private static List<int> FindFileIndexes(ReadOnlySpan<byte> file)
 | 
					        private static List<int> FindFileIndexes(ReadOnlySpan<byte> file)
 | 
				
			||||||
| 
						 | 
					@ -54,7 +41,9 @@ namespace Plpext.Core.PackExtractor
 | 
				
			||||||
                    idx += filePattern.Length;
 | 
					                    idx += filePattern.Length;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                else
 | 
					                else
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
                    idx += skipTable[file[idx + j]];
 | 
					                    idx += skipTable[file[idx + j]];
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            result.Add(file.Length - 1);
 | 
					            result.Add(file.Length - 1);
 | 
				
			||||||
            return result;
 | 
					            return result;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,20 +1,21 @@
 | 
				
			||||||
<Application xmlns="https://github.com/avaloniaui"
 | 
					<Application
 | 
				
			||||||
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 | 
					    x:Class="Plpext.UI.App"
 | 
				
			||||||
             x:Class="Plpext.UI.App"
 | 
					    xmlns="https://github.com/avaloniaui"
 | 
				
			||||||
             RequestedThemeVariant="Dark">
 | 
					    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 | 
				
			||||||
 | 
					    RequestedThemeVariant="Dark">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <Application.Resources>
 | 
					    <Application.Resources>
 | 
				
			||||||
        <ResourceDictionary>
 | 
					        <ResourceDictionary>
 | 
				
			||||||
            <ResourceDictionary.MergedDictionaries>
 | 
					            <ResourceDictionary.MergedDictionaries>
 | 
				
			||||||
                <ResourceInclude Source="avares://Plpext/Resources/Colors.axaml"/>
 | 
					                <ResourceInclude Source="avares://Plpext/Resources/Colors.axaml" />
 | 
				
			||||||
                <ResourceInclude Source="avares://Plpext/Controls/AudioPlayerControl.axaml"/>
 | 
					                <ResourceInclude Source="avares://Plpext/Controls/AudioPlayerControl.axaml" />
 | 
				
			||||||
            </ResourceDictionary.MergedDictionaries>
 | 
					            </ResourceDictionary.MergedDictionaries>
 | 
				
			||||||
        </ResourceDictionary>
 | 
					        </ResourceDictionary>
 | 
				
			||||||
    </Application.Resources>
 | 
					    </Application.Resources>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <Application.Styles>
 | 
					    <Application.Styles>
 | 
				
			||||||
        <FluentTheme />
 | 
					        <FluentTheme />
 | 
				
			||||||
        <StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
 | 
					        <StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
 | 
				
			||||||
        <StyleInclude Source="avares://Plpext/Styles/AudioPlayerControl.axaml"/>
 | 
					        <StyleInclude Source="avares://Plpext/Styles/AudioPlayerControl.axaml" />
 | 
				
			||||||
    </Application.Styles>
 | 
					    </Application.Styles>
 | 
				
			||||||
</Application>
 | 
					</Application>
 | 
				
			||||||
| 
						 | 
					@ -2,74 +2,95 @@
 | 
				
			||||||
    xmlns="https://github.com/avaloniaui"
 | 
					    xmlns="https://github.com/avaloniaui"
 | 
				
			||||||
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 | 
					    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 | 
				
			||||||
    xmlns:controls="using:Plpext.UI"
 | 
					    xmlns:controls="using:Plpext.UI"
 | 
				
			||||||
    xmlns:vm="clr-namespace:Plpext.UI.ViewModels"
 | 
					    xmlns:cv="clr-namespace:Plpext.UI.Controls.Converters"
 | 
				
			||||||
    xmlns:cv="clr-namespace:Plpext.UI.Controls.Converters">
 | 
					    xmlns:vm="clr-namespace:Plpext.UI.ViewModels">
 | 
				
			||||||
 | 
					 | 
				
			||||||
    <!--
 | 
					 | 
				
			||||||
        Additional resources
 | 
					 | 
				
			||||||
        Using Control Themes:
 | 
					 | 
				
			||||||
        https://docs.avaloniaui.net/docs/basics/user-interface/styling/control-themes
 | 
					 | 
				
			||||||
        Using Theme Variants:
 | 
					 | 
				
			||||||
        https://docs.avaloniaui.net/docs/guides/styles-and-resources/how-to-use-theme-variants
 | 
					 | 
				
			||||||
    -->
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <Design.PreviewWith>
 | 
					    <Design.PreviewWith>
 | 
				
			||||||
        <Border Padding="10" Background="Brown">
 | 
					        <Border Padding="10" Background="Brown">
 | 
				
			||||||
        <StackPanel Width="200" Spacing="10">
 | 
					            <StackPanel Width="200" Spacing="10">
 | 
				
			||||||
            <StackPanel Background="{DynamicResource SystemRegionBrush}">
 | 
					                <StackPanel Background="{DynamicResource SystemRegionBrush}">
 | 
				
			||||||
                <controls:AudioPlayerControl IsPlaying="True" PlaybackState="Stopped" />
 | 
					                    <controls:AudioPlayerControl IsPlaying="True" PlaybackState="Stopped" />
 | 
				
			||||||
                <controls:AudioPlayerControl IsPlaying="True" PlaybackState="Paused" />
 | 
					                    <controls:AudioPlayerControl IsPlaying="True" PlaybackState="Paused" />
 | 
				
			||||||
                <controls:AudioPlayerControl IsPlaying="True" PlaybackState="Playing" />
 | 
					                    <controls:AudioPlayerControl IsPlaying="True" PlaybackState="Playing" />
 | 
				
			||||||
                <controls:AudioPlayerControl IsPlaying="False" PlaybackState="Stopped" />
 | 
					                    <controls:AudioPlayerControl IsPlaying="False" PlaybackState="Stopped" />
 | 
				
			||||||
 | 
					                </StackPanel>
 | 
				
			||||||
            </StackPanel>
 | 
					            </StackPanel>
 | 
				
			||||||
        </StackPanel>
 | 
					 | 
				
			||||||
        </Border>
 | 
					        </Border>
 | 
				
			||||||
    </Design.PreviewWith>
 | 
					    </Design.PreviewWith>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <ControlTheme x:Key="{x:Type controls:AudioPlayerControl}" TargetType="controls:AudioPlayerControl"
 | 
					    <ControlTheme
 | 
				
			||||||
                  x:DataType="vm:AudioPlayerViewModel">
 | 
					        x:Key="{x:Type controls:AudioPlayerControl}"
 | 
				
			||||||
 | 
					        x:DataType="vm:AudioPlayerViewModel"
 | 
				
			||||||
 | 
					        TargetType="controls:AudioPlayerControl">
 | 
				
			||||||
        <Setter Property="Template">
 | 
					        <Setter Property="Template">
 | 
				
			||||||
            <ControlTemplate>
 | 
					            <ControlTemplate>
 | 
				
			||||||
                <Grid Name="ButtonGrid" ColumnDefinitions="*,*, Auto">
 | 
					                <Grid Name="ButtonGrid" ColumnDefinitions="*,*, Auto">
 | 
				
			||||||
                    <Grid.Transitions>
 | 
					                    <Grid.Transitions>
 | 
				
			||||||
                        <Transitions>
 | 
					                        <Transitions>
 | 
				
			||||||
                            <ThicknessTransition Property="Margin" Duration="0:0:0.3" Easing="SineEaseInOut"/>
 | 
					                            <ThicknessTransition
 | 
				
			||||||
 | 
					                                Easing="SineEaseInOut"
 | 
				
			||||||
 | 
					                                Property="Margin"
 | 
				
			||||||
 | 
					                                Duration="0:0:0.3" />
 | 
				
			||||||
                        </Transitions>
 | 
					                        </Transitions>
 | 
				
			||||||
                    </Grid.Transitions>
 | 
					                    </Grid.Transitions>
 | 
				
			||||||
                    <Button Name="PlayButton" Grid.Column="0" IsEnabled="{TemplateBinding IsEnabled}" 
 | 
					                    <Button
 | 
				
			||||||
                            HorizontalAlignment="Center" Width="32" Height="32" 
 | 
					                        Name="PlayButton"
 | 
				
			||||||
                            Padding="6 6 6 6"
 | 
					                        Grid.Column="0"
 | 
				
			||||||
                            CornerRadius="12"
 | 
					                        Width="32"
 | 
				
			||||||
                            FontSize="{TemplateBinding FontSize}" 
 | 
					                        Height="32"
 | 
				
			||||||
                            CommandParameter="{TemplateBinding PlayCommandParameter}" Command="{TemplateBinding PlayCommand}">
 | 
					                        Padding="6,6,6,6"
 | 
				
			||||||
 | 
					                        HorizontalAlignment="Center"
 | 
				
			||||||
 | 
					                        Command="{TemplateBinding PlayCommand}"
 | 
				
			||||||
 | 
					                        CommandParameter="{TemplateBinding PlayCommandParameter}"
 | 
				
			||||||
 | 
					                        CornerRadius="12"
 | 
				
			||||||
 | 
					                        FontSize="{TemplateBinding FontSize}"
 | 
				
			||||||
 | 
					                        IsEnabled="{TemplateBinding IsEnabled}">
 | 
				
			||||||
                        <Path Fill="{Binding $parent[Button].Foreground}">
 | 
					                        <Path Fill="{Binding $parent[Button].Foreground}">
 | 
				
			||||||
                            <Path.Data>
 | 
					                            <Path.Data>
 | 
				
			||||||
                                <Binding Path="PlaybackState" 
 | 
					                                <Binding
 | 
				
			||||||
                                         RelativeSource="{RelativeSource TemplatedParent}"
 | 
					                                    Converter="{x:Static cv:PlaybackStateToPathConverter.Instance}"
 | 
				
			||||||
                                         Converter="{x:Static cv:PlaybackStateToPathConverter.Instance}"/>
 | 
					                                    Path="PlaybackState"
 | 
				
			||||||
 | 
					                                    RelativeSource="{RelativeSource TemplatedParent}" />
 | 
				
			||||||
                            </Path.Data>
 | 
					                            </Path.Data>
 | 
				
			||||||
                        </Path>
 | 
					                        </Path>
 | 
				
			||||||
                    </Button>
 | 
					                    </Button>
 | 
				
			||||||
                    <Button Name="StopButton" Grid.Column="1" IsEnabled="{TemplateBinding IsEnabled}" IsVisible="{TemplateBinding IsPlaying}" 
 | 
					                    <Button
 | 
				
			||||||
                            HorizontalAlignment="Center" Width="32" Height="32" 
 | 
					                        Name="StopButton"
 | 
				
			||||||
                            Padding="6 6 6 6"
 | 
					                        Grid.Column="1"
 | 
				
			||||||
                            CornerRadius="12"
 | 
					                        Width="32"
 | 
				
			||||||
                            FontSize="{TemplateBinding FontSize}" 
 | 
					                        Height="32"
 | 
				
			||||||
                            CommandParameter="{TemplateBinding StopCommandParameter}" Command="{TemplateBinding StopCommand}">
 | 
					                        Padding="6,6,6,6"
 | 
				
			||||||
 | 
					                        HorizontalAlignment="Center"
 | 
				
			||||||
 | 
					                        Command="{TemplateBinding StopCommand}"
 | 
				
			||||||
 | 
					                        CommandParameter="{TemplateBinding StopCommandParameter}"
 | 
				
			||||||
 | 
					                        CornerRadius="12"
 | 
				
			||||||
 | 
					                        FontSize="{TemplateBinding FontSize}"
 | 
				
			||||||
 | 
					                        IsEnabled="{TemplateBinding IsEnabled}"
 | 
				
			||||||
 | 
					                        IsVisible="{TemplateBinding IsPlaying}">
 | 
				
			||||||
                        <Path Data="M2,2 H14 V14 H2 Z" Fill="{Binding $parent[Button].Foreground}" />
 | 
					                        <Path Data="M2,2 H14 V14 H2 Z" Fill="{Binding $parent[Button].Foreground}" />
 | 
				
			||||||
                    </Button>
 | 
					                    </Button>
 | 
				
			||||||
                    <StackPanel Grid.Column="2" Name="ProgressPanel" Orientation="Vertical">
 | 
					                    <StackPanel
 | 
				
			||||||
 | 
					                        Name="ProgressPanel"
 | 
				
			||||||
 | 
					                        Grid.Column="2"
 | 
				
			||||||
 | 
					                        Orientation="Vertical">
 | 
				
			||||||
                        <StackPanel.Transitions>
 | 
					                        <StackPanel.Transitions>
 | 
				
			||||||
                            <Transitions>
 | 
					                            <Transitions>
 | 
				
			||||||
                                <DoubleTransition Property="Width" Duration="0:0:0.3" Easing="SineEaseInOut"/>
 | 
					                                <DoubleTransition
 | 
				
			||||||
                                <DoubleTransition Property="Opacity" Duration="0:0:0.3"/>
 | 
					                                    Easing="SineEaseInOut"
 | 
				
			||||||
 | 
					                                    Property="Width"
 | 
				
			||||||
 | 
					                                    Duration="0:0:0.3" />
 | 
				
			||||||
 | 
					                                <DoubleTransition Property="Opacity" Duration="0:0:0.3" />
 | 
				
			||||||
                            </Transitions>
 | 
					                            </Transitions>
 | 
				
			||||||
                        </StackPanel.Transitions>
 | 
					                        </StackPanel.Transitions>
 | 
				
			||||||
                        <ProgressBar Name="ProgressPanelBar" Value="{TemplateBinding Progress}" HorizontalAlignment="Left" CornerRadius="2"/>
 | 
					                        <ProgressBar
 | 
				
			||||||
 | 
					                            Name="ProgressPanelBar"
 | 
				
			||||||
 | 
					                            HorizontalAlignment="Left"
 | 
				
			||||||
 | 
					                            CornerRadius="2"
 | 
				
			||||||
 | 
					                            Value="{TemplateBinding Progress}" />
 | 
				
			||||||
                        <StackPanel Orientation="Horizontal">
 | 
					                        <StackPanel Orientation="Horizontal">
 | 
				
			||||||
                            <Label FontSize="{TemplateBinding FontSize}"  Content="{TemplateBinding CurrentDuration}"/>
 | 
					                            <Label Content="{TemplateBinding CurrentDuration}" FontSize="{TemplateBinding FontSize}" />
 | 
				
			||||||
                            <Label FontSize="{TemplateBinding FontSize}"  Content="/"/>
 | 
					                            <Label Content="/" FontSize="{TemplateBinding FontSize}" />
 | 
				
			||||||
                            <Label FontSize="{TemplateBinding FontSize}"  Content="{TemplateBinding TotalDuration}"/>
 | 
					                            <Label Content="{TemplateBinding TotalDuration}" FontSize="{TemplateBinding FontSize}" />
 | 
				
			||||||
                        </StackPanel>
 | 
					                        </StackPanel>
 | 
				
			||||||
                    </StackPanel>
 | 
					                    </StackPanel>
 | 
				
			||||||
                </Grid>
 | 
					                </Grid>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,10 +7,6 @@ using Plpext.Core.PackExtractor;
 | 
				
			||||||
using Plpext.UI.ViewModels;
 | 
					using Plpext.UI.ViewModels;
 | 
				
			||||||
using Serilog;
 | 
					using Serilog;
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Linq;
 | 
					 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
using Plpext.Core.AudioConverter;
 | 
					using Plpext.Core.AudioConverter;
 | 
				
			||||||
using Plpext.Core.AudioPlayer;
 | 
					using Plpext.Core.AudioPlayer;
 | 
				
			||||||
using Plpext.UI.Services;
 | 
					using Plpext.UI.Services;
 | 
				
			||||||
| 
						 | 
					@ -19,45 +15,42 @@ using Plpext.UI.Services.FileLoader;
 | 
				
			||||||
using Plpext.UI.Services.PlatformStorage;
 | 
					using Plpext.UI.Services.PlatformStorage;
 | 
				
			||||||
using Plpext.UI.Views;
 | 
					using Plpext.UI.Views;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Plpext.UI.DependencyInjection
 | 
					namespace Plpext.UI.DependencyInjection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public static class Container
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public static class Container
 | 
					    private static IServiceProvider? _container;
 | 
				
			||||||
 | 
					    public static IServiceProvider Services
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private static IServiceProvider? _container;
 | 
					        get => _container ?? Register();
 | 
				
			||||||
        public static IServiceProvider Services
 | 
					    }
 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            get => _container ?? Register();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private static IServiceProvider Register()
 | 
					    private static IServiceProvider Register()
 | 
				
			||||||
        {
 | 
					    {
 | 
				
			||||||
            var hostBuilder = Host
 | 
					        var hostBuilder = Host
 | 
				
			||||||
                .CreateDefaultBuilder()
 | 
					            .CreateDefaultBuilder()
 | 
				
			||||||
                .UseSerilog((context, loggerConfiguration) =>
 | 
					            .UseSerilog((context, loggerConfiguration) =>
 | 
				
			||||||
                {
 | 
					            {
 | 
				
			||||||
                    loggerConfiguration.WriteTo.Debug();
 | 
					                loggerConfiguration.WriteTo.Debug();
 | 
				
			||||||
                })
 | 
					            })
 | 
				
			||||||
                .ConfigureServices((context, services) =>
 | 
					            .ConfigureServices((context, services) =>
 | 
				
			||||||
                {
 | 
					            {
 | 
				
			||||||
                    services.AddSingleton<MainWindow>();
 | 
					                services.AddSingleton<MainWindow>();
 | 
				
			||||||
                    services.AddSingleton<MainWindowViewModel>();
 | 
					                services.AddSingleton<MainWindowViewModel>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    services.AddTransient<AudioPlayerViewModel>();
 | 
					                services.AddSingleton<AudioContext>();
 | 
				
			||||||
                    
 | 
					                services.AddScoped<IAudioPlayer, AudioPlayer>();
 | 
				
			||||||
                    services.AddSingleton<AudioContext>();
 | 
					                services.AddSingleton<IAudioConverter, MP3AudioConverter>();
 | 
				
			||||||
                    services.AddScoped<IAudioPlayer, AudioPlayer>();
 | 
					                services.AddSingleton<IMP3Parser, MP3Parser>();
 | 
				
			||||||
                    services.AddSingleton<IAudioConverter, MP3AudioConverter>();
 | 
					                services.AddSingleton<IPackExtractor, CorePackExtractor>();
 | 
				
			||||||
                    services.AddSingleton<IMP3Parser, MP3Parser>();
 | 
					                services.AddSingleton<IFileStorage, DiskFileStorage>();
 | 
				
			||||||
                    services.AddSingleton<IPackExtractor, CorePackExtractor>();
 | 
					                services.AddSingleton<IPlatformStorageService, PlatformStorageService>();
 | 
				
			||||||
                    services.AddSingleton<IFileStorage, DiskFileStorage>();
 | 
					                services.AddSingleton<IFileLoaderService, FileLoaderService>();
 | 
				
			||||||
                    services.AddSingleton<IPlatformStorageService, PlatformStorageService>();
 | 
					                services.AddSingleton<IConvertService, FileConvertService>();
 | 
				
			||||||
                    services.AddSingleton<IFileLoaderService, FileLoaderService>();
 | 
					            })
 | 
				
			||||||
                    services.AddSingleton<IConvertService, FileConvertService>();
 | 
					            .Build();
 | 
				
			||||||
                })
 | 
					        hostBuilder.Start();
 | 
				
			||||||
                .Build();
 | 
					        _container = hostBuilder.Services;
 | 
				
			||||||
            hostBuilder.Start();
 | 
					        return _container;
 | 
				
			||||||
            _container = hostBuilder.Services;
 | 
					 | 
				
			||||||
            return _container;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,10 +12,16 @@
 | 
				
			||||||
    <PublishReadyToRun>true</PublishReadyToRun>
 | 
					    <PublishReadyToRun>true</PublishReadyToRun>
 | 
				
			||||||
  </PropertyGroup>
 | 
					  </PropertyGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'">
 | 
				
			||||||
 | 
					    <RuntimeIdentifier>win-x86</RuntimeIdentifier>
 | 
				
			||||||
 | 
					  </PropertyGroup>
 | 
				
			||||||
 | 
					  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
 | 
				
			||||||
 | 
					    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
 | 
				
			||||||
 | 
					  </PropertyGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
    <Folder Include="Models\" />
 | 
					 | 
				
			||||||
    <AvaloniaResource Include="Assets\**" />
 | 
					    <AvaloniaResource Include="Assets\**" />
 | 
				
			||||||
    <AvaloniaResource Remove="ViewModels\Mocks\**" />
 | 
					    <AvaloniaResource Remove="ViewModels\Mocks\**" />
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
| 
						 | 
					@ -45,15 +51,21 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
    <Compile Remove="ViewModels\Mocks\MockAudioPlayerViewModel.cs" />
 | 
					    <Compile Remove="ViewModels\Mocks\MockAudioPlayerViewModel.cs" />
 | 
				
			||||||
 | 
					    <Compile Remove="publish\**" />
 | 
				
			||||||
 | 
					    <Compile Remove="Releases\**" />
 | 
				
			||||||
    <Compile Remove="ViewModels\Mocks\**" />
 | 
					    <Compile Remove="ViewModels\Mocks\**" />
 | 
				
			||||||
    <Compile Remove="ViewLocator.cs" />
 | 
					    <Compile Remove="ViewLocator.cs" />
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
 | 
					    <AvaloniaXaml Remove="publish\**" />
 | 
				
			||||||
 | 
					    <AvaloniaXaml Remove="Releases\**" />
 | 
				
			||||||
    <AvaloniaXaml Remove="ViewModels\Mocks\**" />
 | 
					    <AvaloniaXaml Remove="ViewModels\Mocks\**" />
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
 | 
					    <EmbeddedResource Remove="publish\**" />
 | 
				
			||||||
 | 
					    <EmbeddedResource Remove="Releases\**" />
 | 
				
			||||||
    <EmbeddedResource Remove="ViewModels\Mocks\**" />
 | 
					    <EmbeddedResource Remove="ViewModels\Mocks\**" />
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -62,13 +74,15 @@
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
 | 
					    <None Remove="publish\**" />
 | 
				
			||||||
 | 
					    <None Remove="Releases\**" />
 | 
				
			||||||
    <None Remove="ViewModels\Mocks\**" />
 | 
					    <None Remove="ViewModels\Mocks\**" />
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
 | 
					<Target Name="PostBuild" AfterTargets="PostBuildEvent">
 | 
				
			||||||
  <ItemGroup Condition="'$(RuntimeIdentifier)' == 'win-x86'">
 | 
					  <ItemGroup>
 | 
				
			||||||
    <OpenAL32Dll Include="$(MSBuildProjectDirectory)\..\..\..\deps\win\x86\OpenAL32.dll" Condition="'$(RuntimeIdentifier)' == 'win-x86'" />
 | 
					    <OpenAL32Dll Include="$(MSBuildProjectDirectory)\..\..\..\deps\win\x86\OpenAL32.dll" Condition="'$(RuntimeIdentifier)' == 'win-x86' or '$(PlatformTarget)' == 'x86'" />
 | 
				
			||||||
    <OpenAL32Dll Include="$(MSBuildProjectDirectory)\..\..\..\deps\win\x64\OpenAL32.dll" Condition="'$(RuntimeIdentifier)' == 'win-x64'" />
 | 
					    <OpenAL32Dll Include="$(MSBuildProjectDirectory)\..\..\..\deps\win\x64\OpenAL32.dll" Condition="'$(RuntimeIdentifier)' == 'win-x64' or '$(PlatformTarget)' == 'x64'" />
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
  <Copy SourceFiles="@(OpenAL32Dll)" DestinationFiles="$(OutputPath)\OpenAL32.dll" />
 | 
					  <Copy SourceFiles="@(OpenAL32Dll)" DestinationFiles="$(OutputPath)\OpenAL32.dll" />
 | 
				
			||||||
</Target>
 | 
					</Target>
 | 
				
			||||||
| 
						 | 
					@ -78,6 +92,6 @@
 | 
				
			||||||
    <OpenAL32Dll Include="$(MSBuildProjectDirectory)\..\..\..\deps\win\x86\OpenAL32.dll" Condition="'$(RuntimeIdentifier)' == 'win-x86'" />
 | 
					    <OpenAL32Dll Include="$(MSBuildProjectDirectory)\..\..\..\deps\win\x86\OpenAL32.dll" Condition="'$(RuntimeIdentifier)' == 'win-x86'" />
 | 
				
			||||||
    <OpenAL32Dll Include="$(MSBuildProjectDirectory)\..\..\..\deps\win\x64\OpenAL32.dll" Condition="'$(RuntimeIdentifier)' == 'win-x64'" />
 | 
					    <OpenAL32Dll Include="$(MSBuildProjectDirectory)\..\..\..\deps\win\x64\OpenAL32.dll" Condition="'$(RuntimeIdentifier)' == 'win-x64'" />
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
  <Copy SourceFiles="@(OpenAL32Dll)" DestinationFiles="$(PublishDir)\OpenAL32.dll"/>
 | 
					  <Copy SourceFiles="@(OpenAL32Dll)" DestinationFiles="$(PublishDir)\OpenAL32.dll" />
 | 
				
			||||||
</Target>
 | 
					</Target>
 | 
				
			||||||
</Project>
 | 
					</Project>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,25 +2,24 @@
 | 
				
			||||||
using Avalonia;
 | 
					using Avalonia;
 | 
				
			||||||
using Velopack;
 | 
					using Velopack;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Plpext.UI
 | 
					namespace Plpext.UI;
 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    internal sealed class Program
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        // Initialization code. Don't use any Avalonia, third-party APIs or any
 | 
					 | 
				
			||||||
        // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
 | 
					 | 
				
			||||||
        // yet and stuff might break.
 | 
					 | 
				
			||||||
        [STAThread]
 | 
					 | 
				
			||||||
        public static void Main(string[] args)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            VelopackApp.Build().Run();
 | 
					 | 
				
			||||||
            BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Avalonia configuration, don't remove; also used by visual designer.
 | 
					internal sealed class Program
 | 
				
			||||||
        public static AppBuilder BuildAvaloniaApp()
 | 
					{
 | 
				
			||||||
            => AppBuilder.Configure<App>()
 | 
					    // Initialization code. Don't use any Avalonia, third-party APIs or any
 | 
				
			||||||
                .UsePlatformDetect()
 | 
					    // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
 | 
				
			||||||
                .WithInterFont()
 | 
					    // yet and stuff might break.
 | 
				
			||||||
                .LogToTrace();
 | 
					    [STAThread]
 | 
				
			||||||
 | 
					    public static void Main(string[] args)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        VelopackApp.Build().Run();
 | 
				
			||||||
 | 
					        BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Avalonia configuration, don't remove; also used by visual designer.
 | 
				
			||||||
 | 
					    public static AppBuilder BuildAvaloniaApp()
 | 
				
			||||||
 | 
					        => AppBuilder.Configure<App>()
 | 
				
			||||||
 | 
					            .UsePlatformDetect()
 | 
				
			||||||
 | 
					            .WithInterFont()
 | 
				
			||||||
 | 
					            .LogToTrace();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,6 @@ using System.Linq;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
using Plpext.Core.AudioPlayer;
 | 
					using Plpext.Core.AudioPlayer;
 | 
				
			||||||
using Plpext.Core.Interfaces;
 | 
					using Plpext.Core.Interfaces;
 | 
				
			||||||
using Plpext.Core.Models;
 | 
					 | 
				
			||||||
using Plpext.UI.ViewModels;
 | 
					using Plpext.UI.ViewModels;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Plpext.UI.Services.FileLoader;
 | 
					namespace Plpext.UI.Services.FileLoader;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,6 @@
 | 
				
			||||||
using Avalonia;
 | 
					using Avalonia;
 | 
				
			||||||
using Avalonia.Controls.ApplicationLifetimes;
 | 
					using Avalonia.Controls.ApplicationLifetimes;
 | 
				
			||||||
using System;
 | 
					 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
using Avalonia.Platform.Storage;
 | 
					using Avalonia.Platform.Storage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +18,7 @@ namespace Plpext.UI.Services.PlatformStorage
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                AllowMultiple = false,
 | 
					                AllowMultiple = false,
 | 
				
			||||||
                Title = "Select Plus Library Pack",
 | 
					                Title = "Select Plus Library Pack",
 | 
				
			||||||
                FileTypeFilter = [new ("Plus Library Pack"){Patterns = ["*.plp"]}],
 | 
					                FileTypeFilter = [new("Plus Library Pack") { Patterns = ["*.plp"] }],
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return filePath.Any() ? filePath[0].Path.AbsolutePath : string.Empty;
 | 
					            return filePath.Any() ? filePath[0].Path.AbsolutePath : string.Empty;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,37 +1,43 @@
 | 
				
			||||||
<Styles xmlns="https://github.com/avaloniaui"
 | 
					<Styles
 | 
				
			||||||
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 | 
					    xmlns="https://github.com/avaloniaui"
 | 
				
			||||||
        xmlns:c="clr-namespace:Plpext.UI"
 | 
					    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 | 
				
			||||||
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
 | 
					    xmlns:c="clr-namespace:Plpext.UI"
 | 
				
			||||||
        xmlns:vm="clr-namespace:Plpext.UI.ViewModels">
 | 
					    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
 | 
				
			||||||
 | 
					    xmlns:vm="clr-namespace:Plpext.UI.ViewModels">
 | 
				
			||||||
    <Design.PreviewWith>
 | 
					    <Design.PreviewWith>
 | 
				
			||||||
        <Border Padding="20" Background="RoyalBlue" BorderBrush="Black" BorderThickness="1">
 | 
					        <Border
 | 
				
			||||||
 | 
					            Padding="20"
 | 
				
			||||||
 | 
					            Background="RoyalBlue"
 | 
				
			||||||
 | 
					            BorderBrush="Black"
 | 
				
			||||||
 | 
					            BorderThickness="1">
 | 
				
			||||||
            <StackPanel>
 | 
					            <StackPanel>
 | 
				
			||||||
                <Border Background="Navy" BorderBrush="White" BorderThickness="1">
 | 
					                <Border
 | 
				
			||||||
                <c:AudioPlayerControl
 | 
					                    Background="Navy"
 | 
				
			||||||
                    x:DataType="vm:AudioPlayerViewModel"
 | 
					                    BorderBrush="White"
 | 
				
			||||||
                    IsPlaying="{Binding IsPlaying}"
 | 
					                    BorderThickness="1">
 | 
				
			||||||
                    PlayCommand="{Binding PlayCommand}"
 | 
					                    <c:AudioPlayerControl
 | 
				
			||||||
                    StopCommand="{Binding StopCommand}"
 | 
					                        x:DataType="vm:AudioPlayerViewModel"
 | 
				
			||||||
                    PlaybackState="{Binding PlaybackState}"
 | 
					                        CurrentDuration="0:15"
 | 
				
			||||||
                TotalDuration="0:22"
 | 
					                        IsPlaying="{Binding IsPlaying}"
 | 
				
			||||||
                CurrentDuration="0:15"
 | 
					                        PlayCommand="{Binding PlayCommand}"
 | 
				
			||||||
                Progress="44.8"
 | 
					                        PlaybackState="{Binding PlaybackState}"
 | 
				
			||||||
                >
 | 
					                        Progress="44.8"
 | 
				
			||||||
                    <c:AudioPlayerControl.DataContext>
 | 
					                        StopCommand="{Binding StopCommand}"
 | 
				
			||||||
                        <vm:AudioPlayerViewModel/>
 | 
					                        TotalDuration="0:22">
 | 
				
			||||||
                    </c:AudioPlayerControl.DataContext>
 | 
					                        <c:AudioPlayerControl.DataContext>
 | 
				
			||||||
                </c:AudioPlayerControl>
 | 
					                            <vm:AudioPlayerViewModel />
 | 
				
			||||||
                    </Border>
 | 
					                        </c:AudioPlayerControl.DataContext>
 | 
				
			||||||
 | 
					                    </c:AudioPlayerControl>
 | 
				
			||||||
 | 
					                </Border>
 | 
				
			||||||
                <c:AudioPlayerControl
 | 
					                <c:AudioPlayerControl
 | 
				
			||||||
                    x:DataType="vm:AudioPlayerViewModel"
 | 
					                    x:DataType="vm:AudioPlayerViewModel"
 | 
				
			||||||
 | 
					                    CurrentDuration="15"
 | 
				
			||||||
                    IsPlaying="True"
 | 
					                    IsPlaying="True"
 | 
				
			||||||
                    PlayCommand="{Binding PlayCommand}"
 | 
					                    PlayCommand="{Binding PlayCommand}"
 | 
				
			||||||
                    TotalDuration="22"
 | 
					 | 
				
			||||||
                    CurrentDuration="15"
 | 
					 | 
				
			||||||
                    Progress="44.8"
 | 
					                    Progress="44.8"
 | 
				
			||||||
                >
 | 
					                    TotalDuration="22">
 | 
				
			||||||
                    <c:AudioPlayerControl.DataContext>
 | 
					                    <c:AudioPlayerControl.DataContext>
 | 
				
			||||||
                        <vm:AudioPlayerViewModel/>
 | 
					                        <vm:AudioPlayerViewModel />
 | 
				
			||||||
                    </c:AudioPlayerControl.DataContext>
 | 
					                    </c:AudioPlayerControl.DataContext>
 | 
				
			||||||
                </c:AudioPlayerControl>
 | 
					                </c:AudioPlayerControl>
 | 
				
			||||||
            </StackPanel>
 | 
					            </StackPanel>
 | 
				
			||||||
| 
						 | 
					@ -39,48 +45,48 @@
 | 
				
			||||||
    </Design.PreviewWith>
 | 
					    </Design.PreviewWith>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <Style Selector="c|AudioPlayerControl">
 | 
					    <Style Selector="c|AudioPlayerControl">
 | 
				
			||||||
        <Setter Property="FontSize" Value="14"/>
 | 
					        <Setter Property="FontSize" Value="14" />
 | 
				
			||||||
        <Setter Property="Margin" Value="0,0,0,0"/>
 | 
					        <Setter Property="Margin" Value="0,0,0,0" />
 | 
				
			||||||
        <Setter Property="Width" Value="176"></Setter>
 | 
					        <Setter Property="Width" Value="176" />
 | 
				
			||||||
        <Setter Property="Height" Value="36"></Setter>
 | 
					        <Setter Property="Height" Value="36" />
 | 
				
			||||||
    </Style>
 | 
					    </Style>
 | 
				
			||||||
    <Style Selector="c|AudioPlayerControl Button">
 | 
					    <Style Selector="c|AudioPlayerControl Button">
 | 
				
			||||||
        <Setter Property="Foreground" Value="{StaticResource SecondaryLightest}"/>
 | 
					        <Setter Property="Foreground" Value="{StaticResource SecondaryLightest}" />
 | 
				
			||||||
        <Setter Property="Background" Value="{StaticResource SecondaryDarkest}"/>
 | 
					        <Setter Property="Background" Value="{StaticResource SecondaryDarkest}" />
 | 
				
			||||||
        <Setter Property="BorderThickness" Value="2"/>
 | 
					        <Setter Property="BorderThickness" Value="2" />
 | 
				
			||||||
        <Setter Property="BorderBrush" Value="{StaticResource SecondaryDark}"/>
 | 
					        <Setter Property="BorderBrush" Value="{StaticResource SecondaryDark}" />
 | 
				
			||||||
    </Style>
 | 
					    </Style>
 | 
				
			||||||
    <Style Selector="c|AudioPlayerControl Button:pointerover">
 | 
					    <Style Selector="c|AudioPlayerControl Button:pointerover">
 | 
				
			||||||
        <Setter Property="Foreground" Value="{StaticResource SecondaryMiddle}"/>
 | 
					        <Setter Property="Foreground" Value="{StaticResource SecondaryMiddle}" />
 | 
				
			||||||
    </Style>
 | 
					    </Style>
 | 
				
			||||||
    <Style Selector="c|AudioPlayerControl Button:pointerover /template/ ContentPresenter">
 | 
					    <Style Selector="c|AudioPlayerControl Button:pointerover /template/ ContentPresenter">
 | 
				
			||||||
        <Setter Property="Background" Value="{StaticResource SecondaryDark}"/>
 | 
					        <Setter Property="Background" Value="{StaticResource SecondaryDark}" />
 | 
				
			||||||
        <Setter Property="BorderThickness" Value="2"/>
 | 
					        <Setter Property="BorderThickness" Value="2" />
 | 
				
			||||||
        <Setter Property="BorderBrush" Value="{StaticResource SecondaryDarkest}"/>
 | 
					        <Setter Property="BorderBrush" Value="{StaticResource SecondaryDarkest}" />
 | 
				
			||||||
    </Style>
 | 
					    </Style>
 | 
				
			||||||
    <Style Selector="c|AudioPlayerControl[IsPlaying=False] /template/ Grid#ButtonGrid">
 | 
					    <Style Selector="c|AudioPlayerControl[IsPlaying=False] /template/ Grid#ButtonGrid">
 | 
				
			||||||
        <Setter Property="Margin" Value="142,0,-32,0"/>
 | 
					        <Setter Property="Margin" Value="142,0,-32,0" />
 | 
				
			||||||
    </Style>
 | 
					    </Style>
 | 
				
			||||||
    <Style Selector="c|AudioPlayerControl[IsPlaying=True] /template/ Grid#ButtonGrid">
 | 
					    <Style Selector="c|AudioPlayerControl[IsPlaying=True] /template/ Grid#ButtonGrid">
 | 
				
			||||||
        <Setter Property="Margin" Value="0,0,0,0"/>
 | 
					        <Setter Property="Margin" Value="0,0,0,0" />
 | 
				
			||||||
    </Style>
 | 
					    </Style>
 | 
				
			||||||
    <Style Selector="c|AudioPlayerControl[IsPlaying=False] /template/ StackPanel#ProgressPanel">
 | 
					    <Style Selector="c|AudioPlayerControl[IsPlaying=False] /template/ StackPanel#ProgressPanel">
 | 
				
			||||||
        <Setter Property="Width" Value="0"/>
 | 
					        <Setter Property="Width" Value="0" />
 | 
				
			||||||
        <Setter Property="Opacity" Value="0"/>
 | 
					        <Setter Property="Opacity" Value="0" />
 | 
				
			||||||
    </Style>
 | 
					    </Style>
 | 
				
			||||||
    <Style Selector="c|AudioPlayerControl[IsPlaying=True] /template/ StackPanel#ProgressPanel">
 | 
					    <Style Selector="c|AudioPlayerControl[IsPlaying=True] /template/ StackPanel#ProgressPanel">
 | 
				
			||||||
        <Setter Property="UseLayoutRounding" Value="True"/>
 | 
					        <Setter Property="UseLayoutRounding" Value="True" />
 | 
				
			||||||
        <Setter Property="Opacity" Value="1"/>
 | 
					        <Setter Property="Opacity" Value="1" />
 | 
				
			||||||
        <Setter Property="Margin" Value="6,10,0,0"/>
 | 
					        <Setter Property="Margin" Value="6,10,0,0" />
 | 
				
			||||||
        <Setter Property="VerticalAlignment" Value="Center"/>
 | 
					        <Setter Property="VerticalAlignment" Value="Center" />
 | 
				
			||||||
    </Style>
 | 
					    </Style>
 | 
				
			||||||
    <Style Selector="c|AudioPlayerControl[IsPlaying=False] /template/ ProgressBar:horizontal">
 | 
					    <Style Selector="c|AudioPlayerControl[IsPlaying=False] /template/ ProgressBar:horizontal">
 | 
				
			||||||
        <Setter Property="MinWidth" Value="0"/>
 | 
					        <Setter Property="MinWidth" Value="0" />
 | 
				
			||||||
        <Setter Property="Width" Value="0"/>
 | 
					        <Setter Property="Width" Value="0" />
 | 
				
			||||||
    </Style>
 | 
					    </Style>
 | 
				
			||||||
    <Style Selector="c|AudioPlayerControl[IsPlaying=True] /template/ ProgressBar:horizontal">
 | 
					    <Style Selector="c|AudioPlayerControl[IsPlaying=True] /template/ ProgressBar:horizontal">
 | 
				
			||||||
        <Setter Property="MinWidth" Value="0"/>
 | 
					        <Setter Property="MinWidth" Value="0" />
 | 
				
			||||||
        <Setter Property="Width" Value="100"/>
 | 
					        <Setter Property="Width" Value="100" />
 | 
				
			||||||
    </Style>
 | 
					    </Style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</Styles>
 | 
					</Styles>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,17 @@
 | 
				
			||||||
<Styles xmlns="https://github.com/avaloniaui"
 | 
					<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 | 
				
			||||||
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 | 
					 | 
				
			||||||
    <Design.PreviewWith>
 | 
					    <Design.PreviewWith>
 | 
				
			||||||
        <Border Padding="20">
 | 
					        <Border Padding="20">
 | 
				
			||||||
            <!-- Add Controls for Previewer Here -->
 | 
					            <Border
 | 
				
			||||||
 | 
					                Width="100"
 | 
				
			||||||
 | 
					                Height="100"
 | 
				
			||||||
 | 
					                Classes="Section" />
 | 
				
			||||||
        </Border>
 | 
					        </Border>
 | 
				
			||||||
    </Design.PreviewWith>
 | 
					    </Design.PreviewWith>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <Style Selector="Border.Section">
 | 
					    <Style Selector="Border.Section">
 | 
				
			||||||
        <Setter Property="BorderBrush" Value="{StaticResource PrimaryForeground}"/>
 | 
					        <Setter Property="BorderBrush" Value="{StaticResource PrimaryForeground}" />
 | 
				
			||||||
        <Setter Property="BorderThickness" Value="1"/>
 | 
					        <Setter Property="BorderThickness" Value="1" />
 | 
				
			||||||
        <Setter Property="BoxShadow" Value="0 0 1 1 Black"/>
 | 
					        <Setter Property="BoxShadow" Value="0 0 1 1 Black" />
 | 
				
			||||||
        <Setter Property="CornerRadius" Value="1"/>
 | 
					        <Setter Property="CornerRadius" Value="1" />
 | 
				
			||||||
    </Style>
 | 
					    </Style>
 | 
				
			||||||
</Styles>
 | 
					</Styles>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,30 +1,32 @@
 | 
				
			||||||
<Styles xmlns="https://github.com/avaloniaui"
 | 
					<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 | 
				
			||||||
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 | 
					 | 
				
			||||||
    <Design.PreviewWith>
 | 
					    <Design.PreviewWith>
 | 
				
			||||||
        <Border Padding="20">
 | 
					        <Border Padding="20">
 | 
				
			||||||
            <StackPanel>
 | 
					            <StackPanel>
 | 
				
			||||||
                <Button Classes="Primary" Content="Primary Button" />
 | 
					                <Button Classes="Primary" Content="Primary Button" />
 | 
				
			||||||
                <Button IsEnabled="False" Classes="Primary" Content="Primary Button" />
 | 
					                <Button
 | 
				
			||||||
            </StackPanel>            <!-- Add Controls for Previewer Here -->
 | 
					                    Classes="Primary"
 | 
				
			||||||
 | 
					                    Content="Primary Button"
 | 
				
			||||||
 | 
					                    IsEnabled="False" />
 | 
				
			||||||
 | 
					            </StackPanel>
 | 
				
			||||||
        </Border>
 | 
					        </Border>
 | 
				
			||||||
    </Design.PreviewWith>
 | 
					    </Design.PreviewWith>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <Style Selector="Button.Primary">
 | 
					    <Style Selector="Button.Primary">
 | 
				
			||||||
        <Setter Property="BorderThickness" Value="1"/>
 | 
					        <Setter Property="BorderThickness" Value="1" />
 | 
				
			||||||
        <Setter Property="BorderBrush" Value="{StaticResource SecondaryDarkestBrush}"/>
 | 
					        <Setter Property="BorderBrush" Value="{StaticResource SecondaryDarkestBrush}" />
 | 
				
			||||||
        <Setter Property="Padding" Value="8 12 8 12"/>
 | 
					        <Setter Property="Padding" Value="8 12 8 12" />
 | 
				
			||||||
        <Setter Property="CornerRadius" Value="4"/>
 | 
					        <Setter Property="CornerRadius" Value="4" />
 | 
				
			||||||
        <Setter Property="Margin" Value="2"/>
 | 
					        <Setter Property="Margin" Value="2" />
 | 
				
			||||||
        <Setter Property="Background" Value="{StaticResource SecondaryDark}"/>
 | 
					        <Setter Property="Background" Value="{StaticResource SecondaryDark}" />
 | 
				
			||||||
        <Setter Property="Foreground" Value="{StaticResource SecondaryLightest}"/>
 | 
					        <Setter Property="Foreground" Value="{StaticResource SecondaryLightest}" />
 | 
				
			||||||
    </Style>
 | 
					    </Style>
 | 
				
			||||||
    <Style Selector="Button.Primary:disabled /template/ ContentPresenter">
 | 
					    <Style Selector="Button.Primary:disabled /template/ ContentPresenter">
 | 
				
			||||||
        <Setter Property="Background" Value="{StaticResource SecondaryDarkest}"/>
 | 
					        <Setter Property="Background" Value="{StaticResource SecondaryDarkest}" />
 | 
				
			||||||
        <Setter Property="Foreground" Value="Gray"/>
 | 
					        <Setter Property="Foreground" Value="Gray" />
 | 
				
			||||||
    </Style>
 | 
					    </Style>
 | 
				
			||||||
    <Style Selector="Button.Primary:pointerover /template/ ContentPresenter">
 | 
					    <Style Selector="Button.Primary:pointerover /template/ ContentPresenter">
 | 
				
			||||||
        <Setter Property="Background" Value="{StaticResource SecondaryLightest}"/>
 | 
					        <Setter Property="Background" Value="{StaticResource SecondaryLightest}" />
 | 
				
			||||||
        <Setter Property="Foreground" Value="{StaticResource SecondaryDarkest}"/>
 | 
					        <Setter Property="Foreground" Value="{StaticResource SecondaryDarkest}" />
 | 
				
			||||||
    </Style>
 | 
					    </Style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</Styles>
 | 
					</Styles>
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,12 @@
 | 
				
			||||||
<Styles xmlns="https://github.com/avaloniaui"
 | 
					<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 | 
				
			||||||
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 | 
					 | 
				
			||||||
    <Design.PreviewWith>
 | 
					    <Design.PreviewWith>
 | 
				
			||||||
        <Border Padding="20">
 | 
					        <Border Padding="20">
 | 
				
			||||||
            <!-- Add Controls for Previewer Here -->
 | 
					            <TextBox Classes="Primary" />
 | 
				
			||||||
            <TextBox Classes="Primary"></TextBox>
 | 
					 | 
				
			||||||
        </Border>
 | 
					        </Border>
 | 
				
			||||||
    </Design.PreviewWith>
 | 
					    </Design.PreviewWith>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <Style Selector="TextBox.Primary">
 | 
					    <Style Selector="TextBox.Primary">
 | 
				
			||||||
        <Setter Property="Height" Value="32"/>
 | 
					        <Setter Property="Height" Value="32" />
 | 
				
			||||||
        
 | 
					 | 
				
			||||||
    </Style>
 | 
					    </Style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</Styles>
 | 
					</Styles>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,9 @@
 | 
				
			||||||
    </Design.PreviewWith>
 | 
					    </Design.PreviewWith>
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    <Style Selector="Window">
 | 
					    <Style Selector="Window">
 | 
				
			||||||
        <Setter Property="Background" Value="{StaticResource PrimaryBackground}"></Setter>
 | 
					        <Setter Property="Background" Value="{StaticResource PrimaryBackground}"/>
 | 
				
			||||||
 | 
					        <Setter Property="CanResize" Value="False"/>
 | 
				
			||||||
 | 
					        <Setter Property="Width" Value="800"/>
 | 
				
			||||||
 | 
					        <Setter Property="Height" Value="450"/>
 | 
				
			||||||
    </Style>
 | 
					    </Style>
 | 
				
			||||||
</Styles>
 | 
					</Styles>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,119 +2,129 @@
 | 
				
			||||||
using Plpext.Core.AudioPlayer;
 | 
					using Plpext.Core.AudioPlayer;
 | 
				
			||||||
using Plpext.Core.Models;
 | 
					using Plpext.Core.Models;
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.ComponentModel;
 | 
					 | 
				
			||||||
using System.Linq;
 | 
					 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
using CommunityToolkit.Mvvm.Input;
 | 
					using CommunityToolkit.Mvvm.Input;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Plpext.UI.ViewModels
 | 
					namespace Plpext.UI.ViewModels;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public partial class AudioPlayerViewModel : ViewModelBase, IDisposable
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public partial class AudioPlayerViewModel : ViewModelBase, IDisposable
 | 
					    private readonly AudioPlayer _audioPlayer = null!;
 | 
				
			||||||
 | 
					    private readonly AudioFile _audioFile = null!;
 | 
				
			||||||
 | 
					    private bool _firstExecution = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public AudioFile AudioFile
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private readonly AudioPlayer _audioPlayer = null!;
 | 
					        get { return _audioFile; }
 | 
				
			||||||
        private readonly AudioFile _audioFile = null!;
 | 
					    }
 | 
				
			||||||
        private bool _firstExecution = true;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public AudioFile AudioFile
 | 
					    public AudioPlayerViewModel()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public AudioPlayerViewModel(AudioPlayer audioPlayer, AudioFile audioFile)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        _audioPlayer = audioPlayer;
 | 
				
			||||||
 | 
					        _audioFile = audioFile;
 | 
				
			||||||
 | 
					        CurrentDuration = "0:00";
 | 
				
			||||||
 | 
					        Name = _audioFile.Name;
 | 
				
			||||||
 | 
					        TotalDuration = $"{_audioFile.Duration:m\\:ss}";
 | 
				
			||||||
 | 
					        audioPlayer.OnProgressUpdated += OnProgressUpdated;
 | 
				
			||||||
 | 
					        audioPlayer.OnPlaybackStopped += OnPlaybackStopped;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void OnPlaybackStopped(object? sender, PlaybackStoppedEventArgs e)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        IsPlaying = false;
 | 
				
			||||||
 | 
					        PlaybackState = PlaybackState.Stopped;
 | 
				
			||||||
 | 
					        CurrentDuration = "0:00";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void OnProgressUpdated(object? sender, PlaybackProgress e)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Progress = e.ProgressPercentage;
 | 
				
			||||||
 | 
					        CurrentDuration = $"{e.CurrentPosition:m\\:ss}";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [ObservableProperty]
 | 
				
			||||||
 | 
					    private bool _isSelected;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [ObservableProperty]
 | 
				
			||||||
 | 
					    private bool _isPlaying;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [ObservableProperty]
 | 
				
			||||||
 | 
					    private string _totalDuration = null!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [ObservableProperty]
 | 
				
			||||||
 | 
					    private string _currentDuration = null!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [ObservableProperty]
 | 
				
			||||||
 | 
					    private string _name = null!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [ObservableProperty]
 | 
				
			||||||
 | 
					    private PlaybackState _playbackState = PlaybackState.Stopped;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    [ObservableProperty]
 | 
				
			||||||
 | 
					    private double _progress;
 | 
				
			||||||
 | 
					    private bool disposedValue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [RelayCommand]
 | 
				
			||||||
 | 
					    private async Task Play()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (IsPlaying)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            get { return _audioFile; }
 | 
					            if (PlaybackState == PlaybackState.Playing)
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        public AudioPlayerViewModel()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        public AudioPlayerViewModel(AudioPlayer audioPlayer, AudioFile audioFile)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            _audioPlayer = audioPlayer;
 | 
					 | 
				
			||||||
            _audioFile = audioFile;
 | 
					 | 
				
			||||||
            CurrentDuration = "0:00";
 | 
					 | 
				
			||||||
            Name = _audioFile.Name;
 | 
					 | 
				
			||||||
            TotalDuration = $"{_audioFile.Duration:m\\:ss}";
 | 
					 | 
				
			||||||
            audioPlayer.OnProgressUpdated += OnProgressUpdated;
 | 
					 | 
				
			||||||
            audioPlayer.OnPlaybackStopped += OnPlaybackStopped;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        private void OnPlaybackStopped(object? sender, PlaybackStoppedEventArgs e)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            IsPlaying = false;
 | 
					 | 
				
			||||||
            PlaybackState = PlaybackState.Stopped;
 | 
					 | 
				
			||||||
            CurrentDuration = "0:00";
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        private void OnProgressUpdated(object? sender, PlaybackProgress e)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            Progress = e.ProgressPercentage;
 | 
					 | 
				
			||||||
            CurrentDuration = $"{e.CurrentPosition:m\\:ss}";
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        [ObservableProperty]
 | 
					 | 
				
			||||||
        private bool _isSelected;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        [ObservableProperty]
 | 
					 | 
				
			||||||
        private bool _isPlaying;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        [ObservableProperty]
 | 
					 | 
				
			||||||
        private string _totalDuration = null!;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        [ObservableProperty]
 | 
					 | 
				
			||||||
        private string _currentDuration = null!;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        [ObservableProperty]
 | 
					 | 
				
			||||||
        private string _name = null!;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        [ObservableProperty] private PlaybackState _playbackState = PlaybackState.Stopped;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        [ObservableProperty]
 | 
					 | 
				
			||||||
        private double _progress;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        [RelayCommand]
 | 
					 | 
				
			||||||
        private async Task Play()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            if (IsPlaying)
 | 
					 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                if (PlaybackState == PlaybackState.Playing)
 | 
					                _audioPlayer.Pause();
 | 
				
			||||||
                {
 | 
					                PlaybackState = PlaybackState.Paused;
 | 
				
			||||||
                    _audioPlayer.Pause();
 | 
					            }
 | 
				
			||||||
                    PlaybackState = PlaybackState.Paused;
 | 
					            else if (PlaybackState == PlaybackState.Paused)
 | 
				
			||||||
                }
 | 
					            {
 | 
				
			||||||
                else if (PlaybackState == PlaybackState.Paused)
 | 
					                _audioPlayer.Resume();
 | 
				
			||||||
                {
 | 
					                PlaybackState = PlaybackState.Playing;
 | 
				
			||||||
                    _audioPlayer.Resume();
 | 
					 | 
				
			||||||
                    PlaybackState = PlaybackState.Playing;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            IsPlaying = true;
 | 
					            return;
 | 
				
			||||||
            if (_firstExecution)
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        IsPlaying = true;
 | 
				
			||||||
 | 
					        if (_firstExecution)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            _firstExecution = false;
 | 
				
			||||||
 | 
					            await _audioPlayer.InitAudioPlayerAsync(_audioFile, true, default);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					            await _audioPlayer.Start();
 | 
				
			||||||
 | 
					        PlaybackState = PlaybackState.Playing;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [RelayCommand]
 | 
				
			||||||
 | 
					    private Task Stop()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        IsPlaying = false;
 | 
				
			||||||
 | 
					        _audioPlayer.Stop();
 | 
				
			||||||
 | 
					        PlaybackState = PlaybackState.Stopped;
 | 
				
			||||||
 | 
					        return Task.CompletedTask;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected virtual void Dispose(bool disposing)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (!disposedValue)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (disposing)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                _firstExecution = false;
 | 
					                _audioPlayer.OnPlaybackStopped -= OnPlaybackStopped;
 | 
				
			||||||
                await _audioPlayer.InitAudioPlayerAsync(_audioFile, true, default);
 | 
					                _audioPlayer.OnProgressUpdated -= OnProgressUpdated;
 | 
				
			||||||
 | 
					                _audioPlayer.Dispose();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            else
 | 
					            disposedValue = true;
 | 
				
			||||||
                await _audioPlayer.Start();
 | 
					 | 
				
			||||||
            PlaybackState = PlaybackState.Playing;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        [RelayCommand]
 | 
					 | 
				
			||||||
        private Task Stop()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            IsPlaying = false;
 | 
					 | 
				
			||||||
            _audioPlayer.Stop();
 | 
					 | 
				
			||||||
            PlaybackState = PlaybackState.Stopped;
 | 
					 | 
				
			||||||
            return Task.CompletedTask;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        public void Dispose()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            _audioPlayer.OnPlaybackStopped -= OnPlaybackStopped;
 | 
					 | 
				
			||||||
            _audioPlayer.OnProgressUpdated -= OnProgressUpdated;
 | 
					 | 
				
			||||||
            _audioPlayer.Dispose();
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public void Dispose()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Dispose(disposing: true);
 | 
				
			||||||
 | 
					        GC.SuppressFinalize(this);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -18,7 +18,9 @@ public partial class MainWindowViewModel : ViewModelBase
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public MainWindowViewModel(IPlatformStorageService platformStorageService, IFileLoaderService fileLoaderService,
 | 
					    public MainWindowViewModel(
 | 
				
			||||||
 | 
					        IPlatformStorageService platformStorageService, 
 | 
				
			||||||
 | 
					        IFileLoaderService fileLoaderService,
 | 
				
			||||||
        IConvertService convertService)
 | 
					        IConvertService convertService)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        _fileLoaderService = fileLoaderService;
 | 
					        _fileLoaderService = fileLoaderService;
 | 
				
			||||||
| 
						 | 
					@ -26,30 +28,43 @@ public partial class MainWindowViewModel : ViewModelBase
 | 
				
			||||||
        _convertService = convertService;
 | 
					        _convertService = convertService;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(LoadFileCommand))]
 | 
					    [ObservableProperty]
 | 
				
			||||||
 | 
					    [NotifyCanExecuteChangedFor(nameof(LoadFileCommand))]
 | 
				
			||||||
    private string _originPath = null!;
 | 
					    private string _originPath = null!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConvertAllFilesCommand),nameof(ConvertSelectedFilesCommand))]
 | 
					    [ObservableProperty]
 | 
				
			||||||
 | 
					    [NotifyCanExecuteChangedFor(nameof(ConvertAllFilesCommand), nameof(ConvertSelectedFilesCommand))]
 | 
				
			||||||
    private string _targetPath = null!;
 | 
					    private string _targetPath = null!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [ObservableProperty] private int _totalFilesToExtract;
 | 
					    [ObservableProperty]
 | 
				
			||||||
 | 
					    private int _totalFilesToExtract;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [ObservableProperty] private int _filesReady;
 | 
					    [ObservableProperty]
 | 
				
			||||||
 | 
					    private int _filesReady;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [ObservableProperty] private string _progressBarText = null!;
 | 
					    [ObservableProperty]
 | 
				
			||||||
 | 
					    private string _progressBarText = null!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [ObservableProperty] private double _progressBarValue;
 | 
					    [ObservableProperty]
 | 
				
			||||||
 | 
					    private double _progressBarValue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [ObservableProperty] private bool _isProgressBarIndeterminate;
 | 
					    [ObservableProperty]
 | 
				
			||||||
 | 
					    private bool _isProgressBarIndeterminate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [ObservableProperty] private string _progressBarDetails = null!;
 | 
					    [ObservableProperty]
 | 
				
			||||||
 | 
					    private string _progressBarDetails = null!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConvertAllFilesCommand), nameof(ConvertSelectedFilesCommand), nameof(LoadFileCommand))]
 | 
					    [ObservableProperty]
 | 
				
			||||||
 | 
					    [NotifyCanExecuteChangedFor(nameof(ConvertAllFilesCommand), nameof(ConvertSelectedFilesCommand), nameof(LoadFileCommand))]
 | 
				
			||||||
    private bool _showProgressBar;
 | 
					    private bool _showProgressBar;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConvertAllFilesCommand),nameof(ConvertSelectedFilesCommand))]
 | 
					    [ObservableProperty]
 | 
				
			||||||
 | 
					    [NotifyCanExecuteChangedFor(nameof(ConvertAllFilesCommand), nameof(ConvertSelectedFilesCommand))]
 | 
				
			||||||
    private ObservableCollection<AudioPlayerViewModel> _audioFiles = new();
 | 
					    private ObservableCollection<AudioPlayerViewModel> _audioFiles = new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private bool CanLoadFile() => !string.IsNullOrEmpty(OriginPath) && !ShowProgressBar;
 | 
				
			||||||
 | 
					    private bool CanConvertAllFiles() => !string.IsNullOrEmpty(TargetPath) && AudioFiles.Any() && !ShowProgressBar;
 | 
				
			||||||
 | 
					    private bool CanConvertSelectFiles() => CanConvertAllFiles();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [RelayCommand(CanExecute = nameof(CanLoadFile))]
 | 
					    [RelayCommand(CanExecute = nameof(CanLoadFile))]
 | 
				
			||||||
    private async Task LoadFile()
 | 
					    private async Task LoadFile()
 | 
				
			||||||
| 
						 | 
					@ -79,9 +94,6 @@ public partial class MainWindowViewModel : ViewModelBase
 | 
				
			||||||
        await Dispatcher.UIThread.InvokeAsync(() => ShowProgressBar = false);
 | 
					        await Dispatcher.UIThread.InvokeAsync(() => ShowProgressBar = false);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private bool CanLoadFile() => !string.IsNullOrEmpty(OriginPath) && !ShowProgressBar;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [RelayCommand]
 | 
					    [RelayCommand]
 | 
				
			||||||
    private async Task SelectOriginPath()
 | 
					    private async Task SelectOriginPath()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
| 
						 | 
					@ -108,10 +120,6 @@ public partial class MainWindowViewModel : ViewModelBase
 | 
				
			||||||
        await Dispatcher.UIThread.InvokeAsync(() => ShowProgressBar = false);
 | 
					        await Dispatcher.UIThread.InvokeAsync(() => ShowProgressBar = false);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private bool CanConvertAllFiles() => !string.IsNullOrEmpty(TargetPath) && AudioFiles.Any() && !ShowProgressBar;
 | 
					 | 
				
			||||||
    private bool CanConvertSelectFiles() => CanConvertAllFiles();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [RelayCommand(CanExecute = nameof(CanConvertSelectFiles))]
 | 
					    [RelayCommand(CanExecute = nameof(CanConvertSelectFiles))]
 | 
				
			||||||
    private async Task ConvertSelectedFiles()
 | 
					    private async Task ConvertSelectedFiles()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,7 @@
 | 
				
			||||||
using CommunityToolkit.Mvvm.ComponentModel;
 | 
					using CommunityToolkit.Mvvm.ComponentModel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Plpext.UI.ViewModels
 | 
					namespace Plpext.UI.ViewModels;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class ViewModelBase : ObservableObject
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public class ViewModelBase : ObservableObject
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -9,8 +9,6 @@
 | 
				
			||||||
    Title="Plpext"
 | 
					    Title="Plpext"
 | 
				
			||||||
    d:DesignHeight="450"
 | 
					    d:DesignHeight="450"
 | 
				
			||||||
    d:DesignWidth="800"
 | 
					    d:DesignWidth="800"
 | 
				
			||||||
    Width="800"
 | 
					 | 
				
			||||||
    Height="450"
 | 
					 | 
				
			||||||
    x:DataType="vm:MainWindowViewModel"
 | 
					    x:DataType="vm:MainWindowViewModel"
 | 
				
			||||||
    Icon="/Assets/plpext.png"
 | 
					    Icon="/Assets/plpext.png"
 | 
				
			||||||
    mc:Ignorable="d">
 | 
					    mc:Ignorable="d">
 | 
				
			||||||
| 
						 | 
					@ -24,19 +22,17 @@
 | 
				
			||||||
    </Window.Styles>
 | 
					    </Window.Styles>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <Design.DataContext>
 | 
					    <Design.DataContext>
 | 
				
			||||||
        <!--
 | 
					 | 
				
			||||||
            This only sets the DataContext for the previewer in an IDE,
 | 
					 | 
				
			||||||
            to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs)
 | 
					 | 
				
			||||||
        -->
 | 
					 | 
				
			||||||
        <vm:MainWindowViewModel />
 | 
					        <vm:MainWindowViewModel />
 | 
				
			||||||
    </Design.DataContext>
 | 
					    </Design.DataContext>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <Grid
 | 
					    <Grid
 | 
				
			||||||
        Margin="16, 8, 16, 8"
 | 
					        Margin="16,8,16,8"
 | 
				
			||||||
        ColumnDefinitions="300,32,*"
 | 
					        ColumnDefinitions="300,32,*"
 | 
				
			||||||
        RowDefinitions="Auto,Auto,*">
 | 
					        RowDefinitions="Auto,Auto,*">
 | 
				
			||||||
        <Border Grid.Row="0"
 | 
					        <Border
 | 
				
			||||||
                Grid.Column="0" Classes="Section">
 | 
					            Grid.Row="0"
 | 
				
			||||||
 | 
					            Grid.Column="0"
 | 
				
			||||||
 | 
					            Classes="Section">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <StackPanel Orientation="Vertical">
 | 
					            <StackPanel Orientation="Vertical">
 | 
				
			||||||
                <Grid
 | 
					                <Grid
 | 
				
			||||||
| 
						 | 
					@ -44,10 +40,11 @@
 | 
				
			||||||
                    ColumnDefinitions="*"
 | 
					                    ColumnDefinitions="*"
 | 
				
			||||||
                    RowDefinitions="*, Auto, Auto">
 | 
					                    RowDefinitions="*, Auto, Auto">
 | 
				
			||||||
                    <Label Grid.Row="0" Content="Select your .plp pack file:" />
 | 
					                    <Label Grid.Row="0" Content="Select your .plp pack file:" />
 | 
				
			||||||
                    <StackPanel Height="36"
 | 
					                    <StackPanel
 | 
				
			||||||
                                Grid.Row="1"
 | 
					                        Grid.Row="1"
 | 
				
			||||||
                                Orientation="Horizontal"
 | 
					                        Height="36"
 | 
				
			||||||
                                Spacing="2">
 | 
					                        Orientation="Horizontal"
 | 
				
			||||||
 | 
					                        Spacing="2">
 | 
				
			||||||
                        <TextBox
 | 
					                        <TextBox
 | 
				
			||||||
                            Width="248"
 | 
					                            Width="248"
 | 
				
			||||||
                            HorizontalContentAlignment="Stretch"
 | 
					                            HorizontalContentAlignment="Stretch"
 | 
				
			||||||
| 
						 | 
					@ -59,10 +56,10 @@
 | 
				
			||||||
                        </Button>
 | 
					                        </Button>
 | 
				
			||||||
                    </StackPanel>
 | 
					                    </StackPanel>
 | 
				
			||||||
                    <Button
 | 
					                    <Button
 | 
				
			||||||
                        Height="38"
 | 
					 | 
				
			||||||
                        Grid.Row="2"
 | 
					                        Grid.Row="2"
 | 
				
			||||||
 | 
					                        Height="38"
 | 
				
			||||||
 | 
					                        Margin="0,4,0,0"
 | 
				
			||||||
                        HorizontalAlignment="Right"
 | 
					                        HorizontalAlignment="Right"
 | 
				
			||||||
                        Margin="0 4 0 0"
 | 
					 | 
				
			||||||
                        Classes="Primary"
 | 
					                        Classes="Primary"
 | 
				
			||||||
                        Command="{Binding LoadFileCommand}">
 | 
					                        Command="{Binding LoadFileCommand}">
 | 
				
			||||||
                        <TextBlock VerticalAlignment="Center">Load</TextBlock>
 | 
					                        <TextBlock VerticalAlignment="Center">Load</TextBlock>
 | 
				
			||||||
| 
						 | 
					@ -78,10 +75,10 @@
 | 
				
			||||||
                        Grid.ColumnSpan="2"
 | 
					                        Grid.ColumnSpan="2"
 | 
				
			||||||
                        Content="Select output folder:" />
 | 
					                        Content="Select output folder:" />
 | 
				
			||||||
                    <StackPanel
 | 
					                    <StackPanel
 | 
				
			||||||
                        Height="36"
 | 
					 | 
				
			||||||
                        Grid.Row="1"
 | 
					                        Grid.Row="1"
 | 
				
			||||||
                        Grid.Column="0"
 | 
					                        Grid.Column="0"
 | 
				
			||||||
                        Grid.ColumnSpan="2"
 | 
					                        Grid.ColumnSpan="2"
 | 
				
			||||||
 | 
					                        Height="36"
 | 
				
			||||||
                        Orientation="Horizontal"
 | 
					                        Orientation="Horizontal"
 | 
				
			||||||
                        Spacing="2">
 | 
					                        Spacing="2">
 | 
				
			||||||
                        <TextBox
 | 
					                        <TextBox
 | 
				
			||||||
| 
						 | 
					@ -94,19 +91,19 @@
 | 
				
			||||||
                        </Button>
 | 
					                        </Button>
 | 
				
			||||||
                    </StackPanel>
 | 
					                    </StackPanel>
 | 
				
			||||||
                    <Button
 | 
					                    <Button
 | 
				
			||||||
                        HorizontalAlignment="Left"
 | 
					 | 
				
			||||||
                        Height="38"
 | 
					 | 
				
			||||||
                        Grid.Row="2"
 | 
					                        Grid.Row="2"
 | 
				
			||||||
                        Grid.Column="0"
 | 
					                        Grid.Column="0"
 | 
				
			||||||
 | 
					                        Height="38"
 | 
				
			||||||
 | 
					                        HorizontalAlignment="Left"
 | 
				
			||||||
                        Classes="Primary"
 | 
					                        Classes="Primary"
 | 
				
			||||||
                        Command="{Binding ConvertAllFilesCommand}">
 | 
					                        Command="{Binding ConvertAllFilesCommand}">
 | 
				
			||||||
                        <TextBlock  VerticalAlignment="Center">Extract All</TextBlock>
 | 
					                        <TextBlock VerticalAlignment="Center">Extract All</TextBlock>
 | 
				
			||||||
                    </Button>
 | 
					                    </Button>
 | 
				
			||||||
                    <Button
 | 
					                    <Button
 | 
				
			||||||
                        Height="38"
 | 
					 | 
				
			||||||
                        HorizontalAlignment="Right"
 | 
					 | 
				
			||||||
                        Grid.Row="2"
 | 
					                        Grid.Row="2"
 | 
				
			||||||
                        Grid.Column="1"
 | 
					                        Grid.Column="1"
 | 
				
			||||||
 | 
					                        Height="38"
 | 
				
			||||||
 | 
					                        HorizontalAlignment="Right"
 | 
				
			||||||
                        Classes="Primary"
 | 
					                        Classes="Primary"
 | 
				
			||||||
                        Command="{Binding ConvertSelectedFilesCommand}">
 | 
					                        Command="{Binding ConvertSelectedFilesCommand}">
 | 
				
			||||||
                        <TextBlock VerticalAlignment="Center">Extract Selected</TextBlock>
 | 
					                        <TextBlock VerticalAlignment="Center">Extract Selected</TextBlock>
 | 
				
			||||||
| 
						 | 
					@ -117,12 +114,21 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        </Border>
 | 
					        </Border>
 | 
				
			||||||
        <Grid Grid.Row="2" Grid.Column="0">
 | 
					        <Grid Grid.Row="2" Grid.Column="0">
 | 
				
			||||||
            <StackPanel Orientation="Vertical" VerticalAlignment="Center" Height="60" IsVisible="{Binding ShowProgressBar}">
 | 
					            <StackPanel
 | 
				
			||||||
            <Label Content="{Binding ProgressBarText}"/>
 | 
					                Height="60"
 | 
				
			||||||
 | 
					                VerticalAlignment="Center"
 | 
				
			||||||
 | 
					                IsVisible="{Binding ShowProgressBar}"
 | 
				
			||||||
 | 
					                Orientation="Vertical">
 | 
				
			||||||
 | 
					                <Label Content="{Binding ProgressBarText}" />
 | 
				
			||||||
                <Border Classes="Section">
 | 
					                <Border Classes="Section">
 | 
				
			||||||
                    <ProgressBar Width="298" Orientation="Horizontal" Height="50" IsIndeterminate="{Binding IsProgressBarIndeterminate}" Value="{Binding ProgressBarValue}" />
 | 
					                    <ProgressBar
 | 
				
			||||||
 | 
					                        Width="298"
 | 
				
			||||||
 | 
					                        Height="50"
 | 
				
			||||||
 | 
					                        IsIndeterminate="{Binding IsProgressBarIndeterminate}"
 | 
				
			||||||
 | 
					                        Orientation="Horizontal"
 | 
				
			||||||
 | 
					                        Value="{Binding ProgressBarValue}" />
 | 
				
			||||||
                </Border>
 | 
					                </Border>
 | 
				
			||||||
            <Label Content="{Binding ProgressBarDetails}" HorizontalAlignment="Right"/>
 | 
					                <Label HorizontalAlignment="Right" Content="{Binding ProgressBarDetails}" />
 | 
				
			||||||
            </StackPanel>
 | 
					            </StackPanel>
 | 
				
			||||||
        </Grid>
 | 
					        </Grid>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -136,23 +142,24 @@
 | 
				
			||||||
                </StackPanel>
 | 
					                </StackPanel>
 | 
				
			||||||
                <DataGrid
 | 
					                <DataGrid
 | 
				
			||||||
                    MaxHeight="410"
 | 
					                    MaxHeight="410"
 | 
				
			||||||
                    CanUserSortColumns="False"
 | 
					 | 
				
			||||||
                    CanUserResizeColumns="False"
 | 
					 | 
				
			||||||
                    CanUserReorderColumns="False"
 | 
					 | 
				
			||||||
                    AreRowDetailsFrozen="True"
 | 
					                    AreRowDetailsFrozen="True"
 | 
				
			||||||
                    HeadersVisibility="None"
 | 
					 | 
				
			||||||
                    AutoGenerateColumns="False"
 | 
					                    AutoGenerateColumns="False"
 | 
				
			||||||
                    VerticalScrollBarVisibility="Visible"
 | 
					                    CanUserReorderColumns="False"
 | 
				
			||||||
 | 
					                    CanUserResizeColumns="False"
 | 
				
			||||||
 | 
					                    CanUserSortColumns="False"
 | 
				
			||||||
 | 
					                    HeadersVisibility="None"
 | 
				
			||||||
                    IsReadOnly="False"
 | 
					                    IsReadOnly="False"
 | 
				
			||||||
 | 
					                    ItemsSource="{Binding AudioFiles}"
 | 
				
			||||||
                    SelectionMode="Extended"
 | 
					                    SelectionMode="Extended"
 | 
				
			||||||
                    ItemsSource="{Binding AudioFiles}">
 | 
					                    VerticalScrollBarVisibility="Visible">
 | 
				
			||||||
                    <DataGrid.Columns>
 | 
					                    <DataGrid.Columns>
 | 
				
			||||||
                        <DataGridTemplateColumn Width="36">
 | 
					                        <DataGridTemplateColumn Width="36">
 | 
				
			||||||
                            <DataGridTemplateColumn.CellTemplate>
 | 
					                            <DataGridTemplateColumn.CellTemplate>
 | 
				
			||||||
                                <DataTemplate>
 | 
					                                <DataTemplate>
 | 
				
			||||||
                                    <CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}"
 | 
					                                    <CheckBox
 | 
				
			||||||
                                              HorizontalAlignment="Center"
 | 
					                                        HorizontalAlignment="Center"
 | 
				
			||||||
                                              VerticalAlignment="Center" />
 | 
					                                        VerticalAlignment="Center"
 | 
				
			||||||
 | 
					                                        IsChecked="{Binding IsSelected, Mode=TwoWay}" />
 | 
				
			||||||
                                </DataTemplate>
 | 
					                                </DataTemplate>
 | 
				
			||||||
                            </DataGridTemplateColumn.CellTemplate>
 | 
					                            </DataGridTemplateColumn.CellTemplate>
 | 
				
			||||||
                        </DataGridTemplateColumn>
 | 
					                        </DataGridTemplateColumn>
 | 
				
			||||||
| 
						 | 
					@ -167,14 +174,14 @@
 | 
				
			||||||
                            <DataGridTemplateColumn.CellTemplate>
 | 
					                            <DataGridTemplateColumn.CellTemplate>
 | 
				
			||||||
                                <DataTemplate DataType="vm:AudioPlayerViewModel">
 | 
					                                <DataTemplate DataType="vm:AudioPlayerViewModel">
 | 
				
			||||||
                                    <c:AudioPlayerControl
 | 
					                                    <c:AudioPlayerControl
 | 
				
			||||||
                                        Margin="0 0 16 0"
 | 
					                                        Margin="0,0,16,0"
 | 
				
			||||||
                                        CurrentDuration="{Binding CurrentDuration}"
 | 
					                                        CurrentDuration="{Binding CurrentDuration}"
 | 
				
			||||||
                                        TotalDuration="{Binding TotalDuration}"
 | 
					 | 
				
			||||||
                                        PlayCommand="{Binding PlayCommand}"
 | 
					 | 
				
			||||||
                                        StopCommand="{Binding StopCommand}"
 | 
					 | 
				
			||||||
                                        IsPlaying="{Binding IsPlaying}"
 | 
					                                        IsPlaying="{Binding IsPlaying}"
 | 
				
			||||||
 | 
					                                        PlayCommand="{Binding PlayCommand}"
 | 
				
			||||||
                                        PlaybackState="{Binding PlaybackState}"
 | 
					                                        PlaybackState="{Binding PlaybackState}"
 | 
				
			||||||
                                        Progress="{Binding Progress}" />
 | 
					                                        Progress="{Binding Progress}"
 | 
				
			||||||
 | 
					                                        StopCommand="{Binding StopCommand}"
 | 
				
			||||||
 | 
					                                        TotalDuration="{Binding TotalDuration}" />
 | 
				
			||||||
                                </DataTemplate>
 | 
					                                </DataTemplate>
 | 
				
			||||||
                            </DataGridTemplateColumn.CellTemplate>
 | 
					                            </DataGridTemplateColumn.CellTemplate>
 | 
				
			||||||
                        </DataGridTemplateColumn>
 | 
					                        </DataGridTemplateColumn>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +0,0 @@
 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    "version": "1.0.0",
 | 
					 | 
				
			||||||
    "executable": "Plpext.exe",
 | 
					 | 
				
			||||||
    "iconFile": "Assets/plpext.png",
 | 
					 | 
				
			||||||
    "splashImage": "Assets/plpext.png"
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
| 
						 | 
					@ -9,34 +9,24 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plpext.Core", "..\Plpext.Co
 | 
				
			||||||
EndProject
 | 
					EndProject
 | 
				
			||||||
Global
 | 
					Global
 | 
				
			||||||
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 | 
						GlobalSection(SolutionConfigurationPlatforms) = preSolution
 | 
				
			||||||
		Debug|Any CPU = Debug|Any CPU
 | 
					 | 
				
			||||||
		Debug|x64 = Debug|x64
 | 
							Debug|x64 = Debug|x64
 | 
				
			||||||
		Debug|x86 = Debug|x86
 | 
							Debug|x86 = Debug|x86
 | 
				
			||||||
		Release|Any CPU = Release|Any CPU
 | 
					 | 
				
			||||||
		Release|x64 = Release|x64
 | 
							Release|x64 = Release|x64
 | 
				
			||||||
		Release|x86 = Release|x86
 | 
							Release|x86 = Release|x86
 | 
				
			||||||
	EndGlobalSection
 | 
						EndGlobalSection
 | 
				
			||||||
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
 | 
						GlobalSection(ProjectConfigurationPlatforms) = postSolution
 | 
				
			||||||
		{CEC79B8A-12B4-4649-B859-08051B00FA96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 | 
					 | 
				
			||||||
		{CEC79B8A-12B4-4649-B859-08051B00FA96}.Debug|Any CPU.Build.0 = Debug|Any CPU
 | 
					 | 
				
			||||||
		{CEC79B8A-12B4-4649-B859-08051B00FA96}.Debug|x64.ActiveCfg = Debug|x64
 | 
							{CEC79B8A-12B4-4649-B859-08051B00FA96}.Debug|x64.ActiveCfg = Debug|x64
 | 
				
			||||||
		{CEC79B8A-12B4-4649-B859-08051B00FA96}.Debug|x64.Build.0 = Debug|x64
 | 
							{CEC79B8A-12B4-4649-B859-08051B00FA96}.Debug|x64.Build.0 = Debug|x64
 | 
				
			||||||
		{CEC79B8A-12B4-4649-B859-08051B00FA96}.Debug|x86.ActiveCfg = Debug|x86
 | 
							{CEC79B8A-12B4-4649-B859-08051B00FA96}.Debug|x86.ActiveCfg = Debug|x86
 | 
				
			||||||
		{CEC79B8A-12B4-4649-B859-08051B00FA96}.Debug|x86.Build.0 = Debug|x86
 | 
							{CEC79B8A-12B4-4649-B859-08051B00FA96}.Debug|x86.Build.0 = Debug|x86
 | 
				
			||||||
		{CEC79B8A-12B4-4649-B859-08051B00FA96}.Release|Any CPU.ActiveCfg = Release|Any CPU
 | 
					 | 
				
			||||||
		{CEC79B8A-12B4-4649-B859-08051B00FA96}.Release|Any CPU.Build.0 = Release|Any CPU
 | 
					 | 
				
			||||||
		{CEC79B8A-12B4-4649-B859-08051B00FA96}.Release|x64.ActiveCfg = Release|x64
 | 
							{CEC79B8A-12B4-4649-B859-08051B00FA96}.Release|x64.ActiveCfg = Release|x64
 | 
				
			||||||
		{CEC79B8A-12B4-4649-B859-08051B00FA96}.Release|x64.Build.0 = Release|x64
 | 
							{CEC79B8A-12B4-4649-B859-08051B00FA96}.Release|x64.Build.0 = Release|x64
 | 
				
			||||||
		{CEC79B8A-12B4-4649-B859-08051B00FA96}.Release|x86.ActiveCfg = Release|x86
 | 
							{CEC79B8A-12B4-4649-B859-08051B00FA96}.Release|x86.ActiveCfg = Release|x86
 | 
				
			||||||
		{CEC79B8A-12B4-4649-B859-08051B00FA96}.Release|x86.Build.0 = Release|x86
 | 
							{CEC79B8A-12B4-4649-B859-08051B00FA96}.Release|x86.Build.0 = Release|x86
 | 
				
			||||||
		{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 | 
					 | 
				
			||||||
		{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Debug|Any CPU.Build.0 = Debug|Any CPU
 | 
					 | 
				
			||||||
		{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Debug|x64.ActiveCfg = Debug|x64
 | 
							{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Debug|x64.ActiveCfg = Debug|x64
 | 
				
			||||||
		{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Debug|x64.Build.0 = Debug|x64
 | 
							{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Debug|x64.Build.0 = Debug|x64
 | 
				
			||||||
		{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Debug|x86.ActiveCfg = Debug|x86
 | 
							{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Debug|x86.ActiveCfg = Debug|x86
 | 
				
			||||||
		{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Debug|x86.Build.0 = Debug|x86
 | 
							{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Debug|x86.Build.0 = Debug|x86
 | 
				
			||||||
		{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Release|Any CPU.ActiveCfg = Release|Any CPU
 | 
					 | 
				
			||||||
		{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Release|Any CPU.Build.0 = Release|Any CPU
 | 
					 | 
				
			||||||
		{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Release|x64.ActiveCfg = Release|x64
 | 
							{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Release|x64.ActiveCfg = Release|x64
 | 
				
			||||||
		{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Release|x64.Build.0 = Release|x64
 | 
							{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Release|x64.Build.0 = Release|x64
 | 
				
			||||||
		{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Release|x86.ActiveCfg = Release|x86
 | 
							{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Release|x86.ActiveCfg = Release|x86
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue