Performed some basic linting.
Some checks are pending
CI/CD / build-windows (push) Waiting to run

This commit is contained in:
Felipe Cotti 2024-11-30 18:17:52 -03:00
parent 2a8166a0c5
commit d881ed69e2
33 changed files with 580 additions and 640 deletions

View file

@ -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
{
private readonly IMP3Parser _parser;
public MP3AudioConverter(IMP3Parser parser) public class MP3AudioConverter : IAudioConverter
{
private readonly IMP3Parser _parser;
public MP3AudioConverter(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;
try
{ {
_parser = parser; while (bytesReturned > 0)
}
public async Task<AudioFile> ConvertAudioAsync(ReadOnlyMemory<byte> file, CancellationToken cancellationToken)
{
var baseFile = await _parser.ParseIntoMP3(file, cancellationToken);
byte[] pcmData = null!;
using var mp3Stream = new MP3Stream(new MemoryStream(baseFile.Data.ToArray()));
using var pcmStream = new MemoryStream();
var buffer = new byte[4096];
int bytesReturned = 1;
int totalBytesRead = 0;
try
{ {
while (bytesReturned > 0) try
{ {
try bytesReturned = await mp3Stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
{
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 (IndexOutOfRangeException)
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}"); //File reached a corrupted/non-compliant portion.
Console.WriteLine($"InnerException StackTrace: {ex.InnerException.StackTrace}"); break;
} }
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);
} }
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;
} }
} }

View file

@ -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;
@ -35,19 +28,18 @@ public sealed class AudioPlayer : IAudioPlayer, IDisposable
{ {
int bufferId = AL.GenBuffer(); int bufferId = AL.GenBuffer();
int sourceId = AL.GenSource(); int sourceId = AL.GenSource();
_currentBufferId = bufferId; _currentBufferId = bufferId;
_currentSourceId = sourceId; _currentSourceId = sourceId;
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;
} }

View file

@ -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;
}
} }

View file

@ -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

View file

@ -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;

View file

@ -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);
}
} }

View file

@ -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);
}
} }

View file

@ -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
{ {

View file

@ -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 });
}
} }
} }

View file

@ -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; }
}
} }

View file

@ -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
{ {

View file

@ -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; }
} }

View file

@ -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; }
}
} }

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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>

View file

@ -2,81 +2,102 @@
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>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</ControlTheme> </ControlTheme>
</ResourceDictionary> </ResourceDictionary>

View file

@ -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;
}
} }
} }

View file

@ -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>

View file

@ -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();
} }

View file

@ -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;
@ -14,7 +13,7 @@ public class FileLoaderService : IFileLoaderService
private readonly IPackExtractor _packExtractor; private readonly IPackExtractor _packExtractor;
private readonly IAudioConverter _audioConverter; private readonly IAudioConverter _audioConverter;
private IEnumerable<ReadOnlyMemory<byte>> _currentFile = null!; private IEnumerable<ReadOnlyMemory<byte>> _currentFile = null!;
public FileLoaderService(IPackExtractor packExtractor, IAudioConverter audioConverter) public FileLoaderService(IPackExtractor packExtractor, IAudioConverter audioConverter)
{ {
_packExtractor = packExtractor; _packExtractor = packExtractor;

View file

@ -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,12 +18,12 @@ 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;
} }
public async Task<string> GetTargetFolderPath() public async Task<string> GetTargetFolderPath()
{ {
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop ||

View file

@ -1,86 +1,92 @@
<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>
</Border> </Border>
</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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 AudioPlayerViewModel()
public AudioFile AudioFile {
}
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);
}
} }

View file

@ -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()
{ {

View file

@ -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 }
{
}
}

View file

@ -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>

View file

@ -1,6 +0,0 @@
{
"version": "1.0.0",
"executable": "Plpext.exe",
"iconFile": "Assets/plpext.png",
"splashImage": "Assets/plpext.png"
}

View file

@ -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