Initial implementation of the UI.

This commit is contained in:
Felipe Cotti 2024-11-30 03:32:15 -03:00
parent 7f19615830
commit 182e473d1b
37 changed files with 1468 additions and 9 deletions

36
.github/main.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: CI/CD
on:
push:
branches:
- main
- '*'
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Install tools
run: dotnet tool install --global vpk
- name: Dotnet publish
run: dotnet publish src/Plpext/Plpext.UI/Plpext.UI.csproj -c Release -r win-x64 -o publish
- name: Vpk pack
run: vpk pack --mainExe=Plpext.exe --packId=Plpext --packVersion=1.0.0 --packDir=publish -o build/win
- name: Upload artifact
uses: softprops/action-gh-release@v2
with:
files: build/win/Plpext-win-Setup.exe

3
.gitignore vendored
View file

@ -8,4 +8,5 @@ artifacts/
*.suo *.suo
*.userprefs *.userprefs
*DS_Store *DS_Store
*.sln.ide *.sln.ide
/build/win

View file

@ -4,14 +4,14 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Platforms>x86;x64</Platforms> <Platforms>x86;x64;AnyCPU</Platforms>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Plpext.Core\Plpext.Core.csproj" /> <ProjectReference Include="..\Plpext.Core\Plpext.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(Platform)' == 'x86' Or '$(PlatformTarget)' == 'x86'"> <ItemGroup Condition="'$(Platform)' == 'x86' Or '$(PlatformTarget)' == 'x86' Or '$(Platform)' == 'AnyCPU' Or '$(PlatformTarget)' == 'AnyCPU' ">
<None Include="lib\x86\OpenAL32.dll"> <None Include="lib\x86\OpenAL32.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
<TargetPath>OpenAL32.dll</TargetPath> <TargetPath>OpenAL32.dll</TargetPath>

View file

@ -61,7 +61,7 @@ namespace Plpext.Core.AudioConverter
var audioDuration = (double)(pcmData.Length / 2) / mp3Stream.Frequency; var audioDuration = (double)(pcmData.Length / 2) / mp3Stream.Frequency;
return new AudioFile() { Name = baseFile.Name, Data = pcmData, Duration = TimeSpan.FromSeconds(audioDuration), Format = AudioFormat.Mono16, Frequency = mp3Stream.Frequency }; return new AudioFile() { Name = baseFile.Name, MP3Data = file, Data = pcmData, Duration = TimeSpan.FromSeconds(audioDuration), Format = AudioFormat.Mono16, Frequency = mp3Stream.Frequency };
} }
private static byte[] ResampleToMono(Span<byte> data) private static byte[] ResampleToMono(Span<byte> data)

View file

@ -26,23 +26,24 @@ public sealed class AudioPlayer : IAudioPlayer, IDisposable
public PlaybackState State { get; private set; } = PlaybackState.Stopped; public PlaybackState State { get; private set; } = PlaybackState.Stopped;
public async Task<bool> InitAudioPlayerAsync(AudioFile input, CancellationToken cancellationToken) public async Task<bool> InitAudioPlayerAsync(AudioFile input, bool autoStart, CancellationToken cancellationToken)
{ {
_audioFile = input; _audioFile = input;
_playbackCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _playbackCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
try try
{ {
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);
return await Start(); if(autoStart)
return await Start();
return true;
} }
catch (Exception ex) catch (Exception ex)
{ {

View file

@ -9,7 +9,7 @@ namespace Plpext.Core.Interfaces;
public interface IAudioPlayer public interface IAudioPlayer
{ {
Task<bool> InitAudioPlayerAsync(AudioFile input, CancellationToken cancellationToken); Task<bool> InitAudioPlayerAsync(AudioFile input, bool autoStart, CancellationToken cancellationToken);
void Resume(); void Resume();
void Pause(); void Pause();
void Stop(); void Stop();

View file

@ -9,6 +9,7 @@ namespace Plpext.Core.Models
public record AudioFile public record AudioFile
{ {
public required string Name { get; init; } public required string Name { get; init; }
public ReadOnlyMemory<byte> MP3Data { get; init; }
public ReadOnlyMemory<byte> Data { get; init; } public ReadOnlyMemory<byte> Data { get; init; }
public TimeSpan Duration { get; init; } public TimeSpan Duration { get; init; }
public AudioFormat Format { get; init; } public AudioFormat Format { get; init; }

View file

@ -0,0 +1,20 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Plpext.UI.App"
RequestedThemeVariant="Dark">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://Plpext/Resources/Colors.axaml"/>
<ResourceInclude Source="avares://Plpext/Controls/AudioPlayerControl.axaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
<StyleInclude Source="avares://Plpext/Styles/AudioPlayerControl.axaml"/>
</Application.Styles>
</Application>

View file

@ -0,0 +1,48 @@
using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using Plpext.Core.AudioPlayer;
using Plpext.UI.DependencyInjection;
using Plpext.UI.ViewModels;
using Plpext.UI.Views;
namespace Plpext.UI
{
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
DisableAvaloniaDataAnnotationValidation();
Container.Services.GetRequiredService<AudioContext>().Initialize();
desktop.MainWindow = Container.Services.GetRequiredService<MainWindow>();
desktop.MainWindow.DataContext = Container.Services.GetRequiredService<MainWindowViewModel>();
}
base.OnFrameworkInitializationCompleted();
}
private void DisableAvaloniaDataAnnotationValidation()
{
var dataValidationPluginsToRemove =
BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();
foreach (var plugin in dataValidationPluginsToRemove)
{
BindingPlugins.DataValidators.Remove(plugin);
}
}
}
}

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,82 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Plpext.UI"
xmlns:vm="clr-namespace:Plpext.UI.ViewModels"
xmlns:cv="clr-namespace:Plpext.UI.Controls.Converters">
<!--
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>
<Border Padding="10" Background="Brown">
<StackPanel Width="200" Spacing="10">
<StackPanel Background="{DynamicResource SystemRegionBrush}">
<controls:AudioPlayerControl IsPlaying="True" PlaybackState="Stopped" />
<controls:AudioPlayerControl IsPlaying="True" PlaybackState="Paused" />
<controls:AudioPlayerControl IsPlaying="True" PlaybackState="Playing" />
<controls:AudioPlayerControl IsPlaying="False" PlaybackState="Stopped" />
</StackPanel>
</StackPanel>
</Border>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type controls:AudioPlayerControl}" TargetType="controls:AudioPlayerControl"
x:DataType="vm:AudioPlayerViewModel">
<Setter Property="Template">
<ControlTemplate>
<Grid Name="ButtonGrid" ColumnDefinitions="*,*, Auto">
<Grid.Transitions>
<Transitions>
<ThicknessTransition Property="Margin" Duration="0:0:0.3" Easing="SineEaseInOut"/>
</Transitions>
</Grid.Transitions>
<Button Name="PlayButton" Grid.Column="0" IsEnabled="{TemplateBinding IsEnabled}"
HorizontalAlignment="Center" Width="32" Height="32"
Padding="6 6 6 6"
CornerRadius="12"
FontSize="{TemplateBinding FontSize}"
CommandParameter="{TemplateBinding PlayCommandParameter}" Command="{TemplateBinding PlayCommand}">
<Path Fill="{Binding $parent[Button].Foreground}">
<Path.Data>
<Binding Path="PlaybackState"
RelativeSource="{RelativeSource TemplatedParent}"
Converter="{x:Static cv:PlaybackStateToPathConverter.Instance}"/>
</Path.Data>
</Path>
</Button>
<Button Name="StopButton" Grid.Column="1" IsEnabled="{TemplateBinding IsEnabled}" IsVisible="{TemplateBinding IsPlaying}"
HorizontalAlignment="Center" Width="32" Height="32"
Padding="6 6 6 6"
CornerRadius="12"
FontSize="{TemplateBinding FontSize}"
CommandParameter="{TemplateBinding StopCommandParameter}" Command="{TemplateBinding StopCommand}">
<Path Data="M2,2 H14 V14 H2 Z" Fill="{Binding $parent[Button].Foreground}" />
</Button>
<StackPanel Grid.Column="2" Name="ProgressPanel" Orientation="Vertical">
<StackPanel.Transitions>
<Transitions>
<DoubleTransition Property="Width" Duration="0:0:0.3" Easing="SineEaseInOut"/>
<DoubleTransition Property="Opacity" Duration="0:0:0.3"/>
</Transitions>
</StackPanel.Transitions>
<ProgressBar Name="ProgressPanelBar" Value="{TemplateBinding Progress}" HorizontalAlignment="Left" CornerRadius="2"/>
<StackPanel Orientation="Horizontal">
<Label FontSize="{TemplateBinding FontSize}" Content="{TemplateBinding CurrentDuration}"/>
<Label FontSize="{TemplateBinding FontSize}" Content="/"/>
<Label FontSize="{TemplateBinding FontSize}" Content="{TemplateBinding TotalDuration}"/>
</StackPanel>
</StackPanel>
</Grid>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

View file

@ -0,0 +1,107 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using System.Windows.Input;
using Avalonia.Data;
using Plpext.Core.Models;
namespace Plpext.UI;
public class AudioPlayerControl : TemplatedControl
{
public static readonly StyledProperty<PlaybackState> PlaybackStateProperty =
AvaloniaProperty.Register<AudioPlayerControl, PlaybackState>(nameof(PlaybackState), defaultValue: PlaybackState.Unknown, inherits: false, defaultBindingMode: BindingMode.OneWay);
public PlaybackState PlaybackState
{
get {return GetValue(PlaybackStateProperty);}
set {SetValue(PlaybackStateProperty, value);}
}
public static readonly StyledProperty<bool> IsPlayingProperty =
AvaloniaProperty.Register<AudioPlayerControl, bool>(nameof(IsPlaying), false, false, Avalonia.Data.BindingMode.TwoWay);
public bool IsPlaying
{
get { return GetValue(IsPlayingProperty); }
set { SetValue(IsPlayingProperty, value); }
}
public static readonly StyledProperty<string> CurrentDurationProperty = AvaloniaProperty.Register<AudioPlayerControl, string>
(
name: nameof(CurrentDuration),
defaultValue: "0:55",
inherits: false,
defaultBindingMode: Avalonia.Data.BindingMode.TwoWay,
validate: (x) => x != "0"
);
public string CurrentDuration
{
get { return GetValue(CurrentDurationProperty); }
set { SetValue(CurrentDurationProperty, value); }
}
public static readonly StyledProperty<string> TotalDurationProperty = AvaloniaProperty.Register<AudioPlayerControl, string>
(
name: nameof(TotalDuration),
defaultValue: "1:07",
inherits: false,
defaultBindingMode: Avalonia.Data.BindingMode.TwoWay,
validate: (x) => x != "0"
);
public string TotalDuration
{
get { return GetValue(TotalDurationProperty); }
set { SetValue(TotalDurationProperty, value); }
}
public static readonly StyledProperty<double> ProgressProperty = AvaloniaProperty.Register<AudioPlayerControl, double>
(
name: nameof(Progress),
defaultValue: 0,
inherits: false,
defaultBindingMode: Avalonia.Data.BindingMode.TwoWay,
validate: (x) => x >= 0
);
public double Progress
{
get { return GetValue(ProgressProperty); }
set { SetValue(ProgressProperty, value); }
}
public static readonly StyledProperty<ICommand?> PlayCommandProperty =
AvaloniaProperty.Register<Button, ICommand?>(nameof(PlayCommand));
public static readonly StyledProperty<object?> PlayCommandParameterProperty =
AvaloniaProperty.Register<Button, object?>(nameof(PlayCommandParameter));
public ICommand? PlayCommand
{
get => GetValue(PlayCommandProperty);
set => SetValue(PlayCommandProperty, value);
}
public object? PlayCommandParameter
{
get => GetValue(PlayCommandParameterProperty);
set => SetValue(PlayCommandParameterProperty, value);
}
public static readonly StyledProperty<ICommand?> StopCommandProperty =
AvaloniaProperty.Register<Button, ICommand?>(nameof(StopCommand));
public static readonly StyledProperty<object?> StopCommandParameterProperty =
AvaloniaProperty.Register<Button, object?>(nameof(StopCommandParameter));
public ICommand? StopCommand
{
get => GetValue(StopCommandProperty);
set => SetValue(StopCommandProperty, value);
}
public object? StopCommandParameter
{
get => GetValue(StopCommandParameterProperty);
set => SetValue(StopCommandParameterProperty, value);
}
}

View file

@ -0,0 +1,29 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
using Plpext.Core.Models;
namespace Plpext.UI.Controls.Converters;
public class PlaybackStateToPathConverter : IValueConverter
{
public static readonly PlaybackStateToPathConverter Instance = new();
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is PlaybackState state)
{
return state == PlaybackState.Playing
? Geometry.Parse("M4,2 H7 V14 H4 Z M11,2 H14 V14 H11 Z")
: Geometry.Parse("M5,2 L5,14 L15,8 Z");
}
return Geometry.Parse("M4,2 L4,14 L14,8 Z");
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,63 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Plpext.Core.FileStorage;
using Plpext.Core.Interfaces;
using Plpext.Core.MP3Parser;
using Plpext.Core.PackExtractor;
using Plpext.UI.ViewModels;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Plpext.Core.AudioConverter;
using Plpext.Core.AudioPlayer;
using Plpext.UI.Services;
using Plpext.UI.Services.Converter;
using Plpext.UI.Services.FileLoader;
using Plpext.UI.Services.PlatformStorage;
using Plpext.UI.Views;
namespace Plpext.UI.DependencyInjection
{
public static class Container
{
private static IServiceProvider? _container;
public static IServiceProvider Services
{
get => _container ?? Register();
}
private static IServiceProvider Register()
{
var hostBuilder = Host
.CreateDefaultBuilder()
.UseSerilog((context, loggerConfiguration) =>
{
loggerConfiguration.WriteTo.Debug();
})
.ConfigureServices((context, services) =>
{
services.AddSingleton<MainWindow>();
services.AddSingleton<MainWindowViewModel>();
services.AddTransient<AudioPlayerViewModel>();
services.AddSingleton<AudioContext>();
services.AddScoped<IAudioPlayer, AudioPlayer>();
services.AddSingleton<IAudioConverter, MP3AudioConverter>();
services.AddSingleton<IMP3Parser, MP3Parser>();
services.AddSingleton<IPackExtractor, CorePackExtractor>();
services.AddSingleton<IFileStorage, DiskFileStorage>();
services.AddSingleton<IPlatformStorageService, PlatformStorageService>();
services.AddSingleton<IFileLoaderService, FileLoaderService>();
services.AddSingleton<IConvertService, FileConvertService>();
})
.Build();
hostBuilder.Start();
_container = hostBuilder.Services;
return _container;
}
}
}

View file

@ -0,0 +1,75 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<Platforms>x86;x64</Platforms>
<AssemblyName>Plpext</AssemblyName>
<SelfContained>true</SelfContained>
<PublishReadyToRun>true</PublishReadyToRun>
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" />
<AvaloniaResource Remove="ViewModels\Mocks\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.2" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.2" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.2" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.2" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.2" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.2">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0-preview3" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
<PackageReference Include="Serilog" Version="4.1.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
<PackageReference Include="Velopack" Version="0.0.942" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Plpext.Core\Plpext.Core.csproj" />
<ProjectReference Include="..\..\Plpext.Core.Windows\Plpext.Core.Windows.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="ViewModels\Mocks\MockAudioPlayerViewModel.cs" />
<Compile Remove="ViewModels\Mocks\**" />
<Compile Remove="ViewLocator.cs" />
</ItemGroup>
<ItemGroup>
<AvaloniaXaml Remove="ViewModels\Mocks\**" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="ViewModels\Mocks\**" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Remove="ViewModels\Mocks\**" />
</ItemGroup>
<ItemGroup>
<None Remove="ViewModels\Mocks\**" />
</ItemGroup>
<Target Name="CopyOpenALToPublish" AfterTargets="Publish">
<Copy SourceFiles="$(MSBuildThisFileDirectory)..\..\Plpext.Core.Windows\bin\x64\Release\net8.0\OpenAL32.dll"
DestinationFolder="$(PublishDir)" />
<Message Text="OpenAL32.dll copied to publish directory: $(PublishDir)" Importance="high" />
</Target>
</Project>

View file

@ -0,0 +1,26 @@
using System;
using Avalonia;
using Velopack;
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.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}
}

View file

@ -0,0 +1,18 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Color x:Key="PrimaryBackground">#020615</Color>
<Color x:Key="PrimaryMiddle">#2D3C81</Color>
<Color x:Key="PrimaryForeground">#99A6E5</Color>
<Color x:Key="SecondaryDarkest">#1F0400</Color>
<Color x:Key="SecondaryDark">#661409</Color>
<Color x:Key="SecondaryMiddle">#BD4131</Color>
<Color x:Key="SecondaryLightest">#FFAA9F</Color>
<SolidColorBrush Color="{StaticResource PrimaryBackground}" x:Key="PrimaryBackgroundBrush"></SolidColorBrush>
<SolidColorBrush Color="{StaticResource PrimaryMiddle}" x:Key="PrimaryMiddleBrush"></SolidColorBrush>
<SolidColorBrush Color="{StaticResource PrimaryForeground}" x:Key="PrimaryForegroundBrush"></SolidColorBrush>
<SolidColorBrush Color="{StaticResource SecondaryDarkest}" x:Key="SecondaryDarkestBrush"></SolidColorBrush>
<SolidColorBrush Color="{StaticResource SecondaryDark}" x:Key="SecondaryDarkBrush"></SolidColorBrush>
<SolidColorBrush Color="{StaticResource SecondaryMiddle}" x:Key="SecondaryMiddleBrush"></SolidColorBrush>
<SolidColorBrush Color="{StaticResource SecondaryLightest}" x:Key="SecondaryLightestBrush"></SolidColorBrush>
</ResourceDictionary>

View file

@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Plpext.Core.Interfaces;
using Plpext.Core.Models;
namespace Plpext.UI.Services.Converter;
public class FileConvertService : IConvertService
{
private readonly IMP3Parser _mp3Parser;
private readonly IFileStorage _fileStorage;
public FileConvertService(IMP3Parser mp3Parser, IFileStorage fileStorage)
{
_mp3Parser = mp3Parser;
_fileStorage = fileStorage;
}
public async Task ConvertFilesAsync(IEnumerable<AudioFile> files, string outputFolder)
{
var mp3Files = new List<MP3File>();
foreach (var file in files)
{
mp3Files.Add(await _mp3Parser.ParseIntoMP3(file.MP3Data, default));
}
await _fileStorage.SaveFilesAsync(mp3Files, outputFolder);
}
}

View file

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Plpext.Core.AudioPlayer;
using Plpext.Core.Interfaces;
using Plpext.Core.Models;
using Plpext.UI.ViewModels;
namespace Plpext.UI.Services.FileLoader;
public class FileLoaderService : IFileLoaderService
{
private readonly IPackExtractor _packExtractor;
private readonly IAudioConverter _audioConverter;
private IEnumerable<ReadOnlyMemory<byte>> _currentFile = null!;
public FileLoaderService(IPackExtractor packExtractor, IAudioConverter audioConverter)
{
_packExtractor = packExtractor;
_audioConverter = audioConverter;
}
public async Task<int> GetFileCountAsync(string filePath)
{
_currentFile = await _packExtractor.GetFileListAsync(filePath, default);
return _currentFile?.Count() ?? 0;
}
public async IAsyncEnumerable<AudioPlayerViewModel> LoadFilesAsync()
{
foreach (var file in _currentFile)
{
var audioFile = await _audioConverter.ConvertAudioAsync(file, default);
yield return new AudioPlayerViewModel(new AudioPlayer(), audioFile);
}
}
}

View file

@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Plpext.Core.Models;
namespace Plpext.UI.Services;
public interface IConvertService
{
public Task ConvertFilesAsync(IEnumerable<AudioFile> files, string outputFolder);
}

View file

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Plpext.UI.ViewModels;
namespace Plpext.UI.Services;
public interface IFileLoaderService
{
Task<int> GetFileCountAsync(string filePath);
IAsyncEnumerable<AudioPlayerViewModel> LoadFilesAsync();
}

View file

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Plpext.UI.Services;
public interface IPlatformStorageService
{
Task<string> GetOriginFilePath();
Task<string> GetTargetFolderPath();
}

View file

@ -0,0 +1,48 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
namespace Plpext.UI.Services.PlatformStorage
{
public class PlatformStorageService : IPlatformStorageService
{
public async Task<string> GetOriginFilePath()
{
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop ||
desktop.MainWindow?.StorageProvider is not { } provider)
return string.Empty;
var filePath = await provider.OpenFilePickerAsync(new FilePickerOpenOptions()
{
AllowMultiple = false,
Title = "Select Plus Library Pack",
FileTypeFilter = [new ("Plus Library Pack"){Patterns = ["*.plp"]}],
});
return filePath.Any() ? filePath[0].Path.AbsolutePath : string.Empty;
}
public async Task<string> GetTargetFolderPath()
{
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop ||
desktop.MainWindow?.StorageProvider is not { } provider)
return string.Empty;
var tentativeFolder = await provider.TryGetWellKnownFolderAsync(Avalonia.Platform.Storage.WellKnownFolder.Music);
var outputPath = await provider.OpenFolderPickerAsync(new Avalonia.Platform.Storage.FolderPickerOpenOptions()
{
AllowMultiple = false,
Title = "Select output folder",
SuggestedStartLocation = tentativeFolder
});
return outputPath.Any() ? outputPath[0].Path.AbsolutePath : string.Empty;
}
}
}

View file

@ -0,0 +1,86 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:Plpext.UI"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:Plpext.UI.ViewModels">
<Design.PreviewWith>
<Border Padding="20" Background="RoyalBlue" BorderBrush="Black" BorderThickness="1">
<StackPanel>
<Border Background="Navy" BorderBrush="White" BorderThickness="1">
<c:AudioPlayerControl
x:DataType="vm:AudioPlayerViewModel"
IsPlaying="{Binding IsPlaying}"
PlayCommand="{Binding PlayCommand}"
StopCommand="{Binding StopCommand}"
PlaybackState="{Binding PlaybackState}"
TotalDuration="0:22"
CurrentDuration="0:15"
Progress="44.8"
>
<c:AudioPlayerControl.DataContext>
<vm:AudioPlayerViewModel/>
</c:AudioPlayerControl.DataContext>
</c:AudioPlayerControl>
</Border>
<c:AudioPlayerControl
x:DataType="vm:AudioPlayerViewModel"
IsPlaying="True"
PlayCommand="{Binding PlayCommand}"
TotalDuration="22"
CurrentDuration="15"
Progress="44.8"
>
<c:AudioPlayerControl.DataContext>
<vm:AudioPlayerViewModel/>
</c:AudioPlayerControl.DataContext>
</c:AudioPlayerControl>
</StackPanel>
</Border>
</Design.PreviewWith>
<Style Selector="c|AudioPlayerControl">
<Setter Property="FontSize" Value="14"/>
<Setter Property="Margin" Value="0,0,0,0"/>
<Setter Property="Width" Value="176"></Setter>
<Setter Property="Height" Value="36"></Setter>
</Style>
<Style Selector="c|AudioPlayerControl Button">
<Setter Property="Foreground" Value="{StaticResource SecondaryLightest}"/>
<Setter Property="Background" Value="{StaticResource SecondaryDarkest}"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="BorderBrush" Value="{StaticResource SecondaryDark}"/>
</Style>
<Style Selector="c|AudioPlayerControl Button:pointerover">
<Setter Property="Foreground" Value="{StaticResource SecondaryMiddle}"/>
</Style>
<Style Selector="c|AudioPlayerControl Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource SecondaryDark}"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="BorderBrush" Value="{StaticResource SecondaryDarkest}"/>
</Style>
<Style Selector="c|AudioPlayerControl[IsPlaying=False] /template/ Grid#ButtonGrid">
<Setter Property="Margin" Value="142,0,-32,0"/>
</Style>
<Style Selector="c|AudioPlayerControl[IsPlaying=True] /template/ Grid#ButtonGrid">
<Setter Property="Margin" Value="0,0,0,0"/>
</Style>
<Style Selector="c|AudioPlayerControl[IsPlaying=False] /template/ StackPanel#ProgressPanel">
<Setter Property="Width" Value="0"/>
<Setter Property="Opacity" Value="0"/>
</Style>
<Style Selector="c|AudioPlayerControl[IsPlaying=True] /template/ StackPanel#ProgressPanel">
<Setter Property="UseLayoutRounding" Value="True"/>
<Setter Property="Opacity" Value="1"/>
<Setter Property="Margin" Value="6,10,0,0"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style Selector="c|AudioPlayerControl[IsPlaying=False] /template/ ProgressBar:horizontal">
<Setter Property="MinWidth" Value="0"/>
<Setter Property="Width" Value="0"/>
</Style>
<Style Selector="c|AudioPlayerControl[IsPlaying=True] /template/ ProgressBar:horizontal">
<Setter Property="MinWidth" Value="0"/>
<Setter Property="Width" Value="100"/>
</Style>
</Styles>

View file

@ -0,0 +1,15 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="20">
<!-- Add Controls for Previewer Here -->
</Border>
</Design.PreviewWith>
<Style Selector="Border.Section">
<Setter Property="BorderBrush" Value="{StaticResource PrimaryForeground}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BoxShadow" Value="0 0 1 1 Black"/>
<Setter Property="CornerRadius" Value="1"/>
</Style>
</Styles>

View file

@ -0,0 +1,30 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="20">
<StackPanel>
<Button Classes="Primary" Content="Primary Button" />
<Button IsEnabled="False" Classes="Primary" Content="Primary Button" />
</StackPanel> <!-- Add Controls for Previewer Here -->
</Border>
</Design.PreviewWith>
<Style Selector="Button.Primary">
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="{StaticResource SecondaryDarkestBrush}"/>
<Setter Property="Padding" Value="8 12 8 12"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="Margin" Value="2"/>
<Setter Property="Background" Value="{StaticResource SecondaryDark}"/>
<Setter Property="Foreground" Value="{StaticResource SecondaryLightest}"/>
</Style>
<Style Selector="Button.Primary:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource SecondaryDarkest}"/>
<Setter Property="Foreground" Value="Gray"/>
</Style>
<Style Selector="Button.Primary:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource SecondaryLightest}"/>
<Setter Property="Foreground" Value="{StaticResource SecondaryDarkest}"/>
</Style>
</Styles>

View file

@ -0,0 +1,14 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="20">
<ProgressBar IsIndeterminate="True"></ProgressBar>
</Border>
</Design.PreviewWith>
<Style Selector="ProgressBar">
<Setter Property="Background" Value="{StaticResource SecondaryDark}"/>
<Setter Property="Foreground" Value="{StaticResource SecondaryLightest}"/>
<Setter Property="CornerRadius" Value="0"/>
</Style>
</Styles>

View file

@ -0,0 +1,15 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="20">
<!-- Add Controls for Previewer Here -->
<TextBox Classes="Primary"></TextBox>
</Border>
</Design.PreviewWith>
<Style Selector="TextBox.Primary">
<Setter Property="Height" Value="32"/>
</Style>
</Styles>

View file

@ -0,0 +1,9 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
</Design.PreviewWith>
<Style Selector="Window">
<Setter Property="Background" Value="{StaticResource PrimaryBackground}"></Setter>
</Style>
</Styles>

View file

@ -0,0 +1,120 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Plpext.Core.AudioPlayer;
using Plpext.Core.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
namespace Plpext.UI.ViewModels
{
public partial class AudioPlayerViewModel : ViewModelBase, IDisposable
{
private readonly AudioPlayer _audioPlayer = null!;
private readonly AudioFile _audioFile = null!;
private bool _firstExecution = true;
public AudioFile AudioFile
{
get { return _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;
[RelayCommand]
private async Task Play()
{
if (IsPlaying)
{
if (PlaybackState == PlaybackState.Playing)
{
_audioPlayer.Pause();
PlaybackState = PlaybackState.Paused;
}
else if (PlaybackState == PlaybackState.Paused)
{
_audioPlayer.Resume();
PlaybackState = PlaybackState.Playing;
}
return;
}
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;
}
public void Dispose()
{
_audioPlayer.OnPlaybackStopped -= OnPlaybackStopped;
_audioPlayer.OnProgressUpdated -= OnProgressUpdated;
_audioPlayer.Dispose();
}
}
}

View file

@ -0,0 +1,129 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Plpext.UI.Services;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Threading;
namespace Plpext.UI.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
private readonly IPlatformStorageService _platformStorageService = null!;
private readonly IFileLoaderService _fileLoaderService = null!;
private readonly IConvertService _convertService = null!;
public MainWindowViewModel()
{
}
public MainWindowViewModel(IPlatformStorageService platformStorageService, IFileLoaderService fileLoaderService,
IConvertService convertService)
{
_fileLoaderService = fileLoaderService;
_platformStorageService = platformStorageService;
_convertService = convertService;
}
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(LoadFileCommand))]
private string _originPath = null!;
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConvertAllFilesCommand),nameof(ConvertSelectedFilesCommand))]
private string _targetPath = null!;
[ObservableProperty] private int _totalFilesToExtract;
[ObservableProperty] private int _filesReady;
[ObservableProperty] private string _progressBarText = null!;
[ObservableProperty] private double _progressBarValue;
[ObservableProperty] private bool _isProgressBarIndeterminate;
[ObservableProperty] private string _progressBarDetails = null!;
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConvertAllFilesCommand), nameof(ConvertSelectedFilesCommand), nameof(LoadFileCommand))]
private bool _showProgressBar;
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConvertAllFilesCommand),nameof(ConvertSelectedFilesCommand))]
private ObservableCollection<AudioPlayerViewModel> _audioFiles = new();
[RelayCommand(CanExecute = nameof(CanLoadFile))]
private async Task LoadFile()
{
TotalFilesToExtract = await _fileLoaderService.GetFileCountAsync(OriginPath);
await Dispatcher.UIThread.InvokeAsync(() =>
{
ProgressBarText = "Loading files...";
ProgressBarValue = 0 / (double)TotalFilesToExtract * 100;
ShowProgressBar = true;
IsProgressBarIndeterminate = false;
});
await foreach (var item in _fileLoaderService.LoadFilesAsync())
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
AudioFiles.Add(item);
FilesReady += 1;
ProgressBarValue = FilesReady / (double)TotalFilesToExtract * 100;
ProgressBarDetails = $"{FilesReady} of {TotalFilesToExtract} ({ProgressBarValue:00.0}%)";
});
}
await Dispatcher.UIThread.InvokeAsync(() => ShowProgressBar = false);
}
private bool CanLoadFile() => !string.IsNullOrEmpty(OriginPath) && !ShowProgressBar;
[RelayCommand]
private async Task SelectOriginPath()
{
OriginPath = await _platformStorageService.GetOriginFilePath();
}
[RelayCommand]
private async Task SelectTargetPath()
{
TargetPath = await _platformStorageService.GetTargetFolderPath();
}
[RelayCommand(CanExecute = nameof(CanConvertAllFiles))]
private async Task ConvertAllFiles()
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
ProgressBarText = $"Converting {TotalFilesToExtract} files...";
IsProgressBarIndeterminate = true;
ShowProgressBar = true;
ProgressBarDetails = string.Empty;
});
await _convertService.ConvertFilesAsync(AudioFiles.Select(x => x.AudioFile), TargetPath);
await Dispatcher.UIThread.InvokeAsync(() => ShowProgressBar = false);
}
private bool CanConvertAllFiles() => !string.IsNullOrEmpty(TargetPath) && AudioFiles.Any() && !ShowProgressBar;
private bool CanConvertSelectFiles() => CanConvertAllFiles();
[RelayCommand(CanExecute = nameof(CanConvertSelectFiles))]
private async Task ConvertSelectedFiles()
{
var filesToConvert = AudioFiles.Where(a => a.IsSelected).Select(x => x.AudioFile).ToArray();
if (filesToConvert.Length == 0) return;
await Dispatcher.UIThread.InvokeAsync(() =>
{
ProgressBarText = $"Converting {filesToConvert.Length} files...";
IsProgressBarIndeterminate = true;
ShowProgressBar = true;
ProgressBarDetails = string.Empty;
});
await _convertService.ConvertFilesAsync(filesToConvert, TargetPath);
await Dispatcher.UIThread.InvokeAsync(() => ShowProgressBar = false);
}
}

View file

@ -0,0 +1,105 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows.Input;
using Plpext.Core.AudioPlayer;
using Plpext.Core.Models;
namespace Plpext.UI.ViewModels.Mocks;
public static class MockAudioPlayerViewModel
{
private static readonly AudioPlayer _audioPlayer = new AudioPlayer();
private static readonly AudioFile _audioFile = new AudioFile()
{ Name = "Dummy", Duration = TimeSpan.FromSeconds(14), Format = AudioFormat.Mono16, Frequency = 44100 };
static MockAudioPlayerViewModel()
{
Instance = new MockAudioPlayerViewModelInstance();
}
public static MockAudioPlayerViewModelInstance Instance { get; }
public class MockAudioPlayerViewModelInstance : INotifyPropertyChanged
{
public MockAudioPlayerViewModelInstance()
{
PlayCommand = new RelayCommand(async () => await Play());
}
private bool _isPlaying;
public bool IsPlaying
{
get => _isPlaying;
set => SetProperty(ref _isPlaying, value);
}
private string _totalDuration;
public string TotalDuration
{
get => _totalDuration;
set => SetProperty(ref _totalDuration, value);
}
private string _currentDuration;
public string CurrentDuration
{
get => _currentDuration;
set => SetProperty(ref _currentDuration, value);
}
private string _name;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public ICommand PlayCommand { get; }
private async Task Play()
{
IsPlaying = !IsPlaying;
// Implement play logic here
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
}
public class RelayCommand : ICommand
{
private readonly Func<Task> _execute;
private readonly Func<bool> _canExecute;
public RelayCommand(Func<Task> execute, Func<bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute == null || _canExecute();
public async void Execute(object parameter) => await _execute();
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}

View file

@ -0,0 +1,187 @@
<Window
x:Class="Plpext.UI.Views.MainWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="using:Plpext.UI"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Plpext.UI.ViewModels"
Title="Plpext"
d:DesignHeight="450"
d:DesignWidth="800"
Width="800"
Height="450"
x:DataType="vm:MainWindowViewModel"
Icon="/Assets/plpext.png"
mc:Ignorable="d">
<Window.Styles>
<StyleInclude Source="/Styles/Button.axaml" />
<StyleInclude Source="/Styles/AudioPlayerControl.axaml" />
<StyleInclude Source="/Styles/Window.axaml" />
<StyleInclude Source="/Styles/Border.axaml" />
<StyleInclude Source="/Styles/TextBox.axaml" />
<StyleInclude Source="/Styles/ProgressBar.axaml" />
</Window.Styles>
<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 />
</Design.DataContext>
<Grid
Margin="16, 8, 16, 8"
ColumnDefinitions="300,32,*"
RowDefinitions="Auto,Auto,*">
<Border Grid.Row="0"
Grid.Column="0" Classes="Section">
<StackPanel Orientation="Vertical">
<Grid
Margin="8"
ColumnDefinitions="*"
RowDefinitions="*, Auto, Auto">
<Label Grid.Row="0" Content="Select your .plp pack file:" />
<StackPanel Height="36"
Grid.Row="1"
Orientation="Horizontal"
Spacing="2">
<TextBox
Width="248"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Center"
Classes="Primary"
Text="{Binding OriginPath}" />
<Button Classes="Primary" Command="{Binding SelectOriginPathCommand}">
<TextBlock Height="24">...</TextBlock>
</Button>
</StackPanel>
<Button
Height="38"
Grid.Row="2"
HorizontalAlignment="Right"
Margin="0 4 0 0"
Classes="Primary"
Command="{Binding LoadFileCommand}">
<TextBlock VerticalAlignment="Center">Load</TextBlock>
</Button>
</Grid>
<Grid
Margin="8"
ColumnDefinitions="*, *"
RowDefinitions="*, Auto, Auto">
<Label
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Content="Select output folder:" />
<StackPanel
Height="36"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Orientation="Horizontal"
Spacing="2">
<TextBox
Width="248"
VerticalContentAlignment="Center"
Classes="Primary"
Text="{Binding TargetPath}" />
<Button Classes="Primary" Command="{Binding SelectTargetPathCommand}">
<TextBlock Height="24">...</TextBlock>
</Button>
</StackPanel>
<Button
HorizontalAlignment="Left"
Height="38"
Grid.Row="2"
Grid.Column="0"
Classes="Primary"
Command="{Binding ConvertAllFilesCommand}">
<TextBlock VerticalAlignment="Center">Extract All</TextBlock>
</Button>
<Button
Height="38"
HorizontalAlignment="Right"
Grid.Row="2"
Grid.Column="1"
Classes="Primary"
Command="{Binding ConvertSelectedFilesCommand}">
<TextBlock VerticalAlignment="Center">Extract Selected</TextBlock>
</Button>
</Grid>
</StackPanel>
</Border>
<Grid Grid.Row="2" Grid.Column="0">
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Height="60" IsVisible="{Binding ShowProgressBar}">
<Label Content="{Binding ProgressBarText}"/>
<Border Classes="Section">
<ProgressBar Width="298" Orientation="Horizontal" Height="50" IsIndeterminate="{Binding IsProgressBarIndeterminate}" Value="{Binding ProgressBarValue}" />
</Border>
<Label Content="{Binding ProgressBarDetails}" HorizontalAlignment="Right"/>
</StackPanel>
</Grid>
<Grid
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="2">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<Label Content="Audio Files" />
</StackPanel>
<DataGrid
MaxHeight="410"
CanUserSortColumns="False"
CanUserResizeColumns="False"
CanUserReorderColumns="False"
AreRowDetailsFrozen="True"
HeadersVisibility="None"
AutoGenerateColumns="False"
VerticalScrollBarVisibility="Visible"
IsReadOnly="False"
SelectionMode="Extended"
ItemsSource="{Binding AudioFiles}">
<DataGrid.Columns>
<DataGridTemplateColumn Width="36">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="vm:AudioPlayerViewModel">
<Label VerticalAlignment="Center" Content="{Binding Name}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="200" MinWidth="200">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="vm:AudioPlayerViewModel">
<c:AudioPlayerControl
Margin="0 0 16 0"
CurrentDuration="{Binding CurrentDuration}"
TotalDuration="{Binding TotalDuration}"
PlayCommand="{Binding PlayCommand}"
StopCommand="{Binding StopCommand}"
IsPlaying="{Binding IsPlaying}"
PlaybackState="{Binding PlaybackState}"
Progress="{Binding Progress}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Grid>
</Grid>
</Window>

View file

@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace Plpext.UI.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="Plpext.UI.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

View file

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

62
src/Plpext/Plpext.sln Normal file
View file

@ -0,0 +1,62 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35521.163
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plpext.UI", "Plpext.UI\Plpext.UI.csproj", "{CEC79B8A-12B4-4649-B859-08051B00FA96}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plpext.Core", "..\Plpext.Core\Plpext.Core.csproj", "{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plpext.Core.Windows", "..\Plpext.Core.Windows\Plpext.Core.Windows.csproj", "{CEF95E1F-8E07-4B32-A5D9-EE980AF5FBC4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
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.Build.0 = Debug|x64
{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}.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.Build.0 = Release|x64
{CEC79B8A-12B4-4649-B859-08051B00FA96}.Release|x86.ActiveCfg = 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.Build.0 = Debug|x64
{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}.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.Build.0 = Release|x64
{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Release|x86.ActiveCfg = Release|x86
{2A0B3CBC-C79F-4B19-93AE-BCEEE44BDAD2}.Release|x86.Build.0 = Release|x86
{CEF95E1F-8E07-4B32-A5D9-EE980AF5FBC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CEF95E1F-8E07-4B32-A5D9-EE980AF5FBC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CEF95E1F-8E07-4B32-A5D9-EE980AF5FBC4}.Debug|x64.ActiveCfg = Debug|x64
{CEF95E1F-8E07-4B32-A5D9-EE980AF5FBC4}.Debug|x64.Build.0 = Debug|x64
{CEF95E1F-8E07-4B32-A5D9-EE980AF5FBC4}.Debug|x86.ActiveCfg = Debug|x86
{CEF95E1F-8E07-4B32-A5D9-EE980AF5FBC4}.Debug|x86.Build.0 = Debug|x86
{CEF95E1F-8E07-4B32-A5D9-EE980AF5FBC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CEF95E1F-8E07-4B32-A5D9-EE980AF5FBC4}.Release|Any CPU.Build.0 = Release|Any CPU
{CEF95E1F-8E07-4B32-A5D9-EE980AF5FBC4}.Release|x64.ActiveCfg = Release|x64
{CEF95E1F-8E07-4B32-A5D9-EE980AF5FBC4}.Release|x64.Build.0 = Release|x64
{CEF95E1F-8E07-4B32-A5D9-EE980AF5FBC4}.Release|x86.ActiveCfg = Release|x86
{CEF95E1F-8E07-4B32-A5D9-EE980AF5FBC4}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal