Initial implementation of the UI.
This commit is contained in:
parent
7f19615830
commit
182e473d1b
37 changed files with 1468 additions and 9 deletions
36
.github/main.yml
vendored
Normal file
36
.github/main.yml
vendored
Normal 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
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -9,3 +9,4 @@ artifacts/
|
||||||
*.userprefs
|
*.userprefs
|
||||||
*DS_Store
|
*DS_Store
|
||||||
*.sln.ide
|
*.sln.ide
|
||||||
|
/build/win
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -26,11 +26,10 @@ 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
|
||||||
{
|
{
|
||||||
|
@ -42,7 +41,9 @@ public sealed class AudioPlayer : IAudioPlayer, IDisposable
|
||||||
|
|
||||||
AL.BufferData(bufferId, ALFormat.Mono16, input.Data.Span, input.Frequency);
|
AL.BufferData(bufferId, ALFormat.Mono16, input.Data.Span, input.Frequency);
|
||||||
AL.Source(sourceId, ALSourcei.Buffer, bufferId);
|
AL.Source(sourceId, ALSourcei.Buffer, bufferId);
|
||||||
return await Start();
|
if(autoStart)
|
||||||
|
return await Start();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
20
src/Plpext/Plpext.UI/App.axaml
Normal file
20
src/Plpext/Plpext.UI/App.axaml
Normal 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>
|
48
src/Plpext/Plpext.UI/App.axaml.cs
Normal file
48
src/Plpext/Plpext.UI/App.axaml.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
82
src/Plpext/Plpext.UI/Controls/AudioPlayerControl.axaml
Normal file
82
src/Plpext/Plpext.UI/Controls/AudioPlayerControl.axaml
Normal 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>
|
107
src/Plpext/Plpext.UI/Controls/AudioPlayerControl.axaml.cs
Normal file
107
src/Plpext/Plpext.UI/Controls/AudioPlayerControl.axaml.cs
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
63
src/Plpext/Plpext.UI/DependencyInjection/Container.cs
Normal file
63
src/Plpext/Plpext.UI/DependencyInjection/Container.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
75
src/Plpext/Plpext.UI/Plpext.UI.csproj
Normal file
75
src/Plpext/Plpext.UI/Plpext.UI.csproj
Normal 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>
|
26
src/Plpext/Plpext.UI/Program.cs
Normal file
26
src/Plpext/Plpext.UI/Program.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
18
src/Plpext/Plpext.UI/Resources/Colors.axaml
Normal file
18
src/Plpext/Plpext.UI/Resources/Colors.axaml
Normal 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>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
src/Plpext/Plpext.UI/Services/IConvertService.cs
Normal file
10
src/Plpext/Plpext.UI/Services/IConvertService.cs
Normal 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);
|
||||||
|
}
|
11
src/Plpext/Plpext.UI/Services/IFileLoaderService.cs
Normal file
11
src/Plpext/Plpext.UI/Services/IFileLoaderService.cs
Normal 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();
|
||||||
|
}
|
9
src/Plpext/Plpext.UI/Services/IPlatformStorageService.cs
Normal file
9
src/Plpext/Plpext.UI/Services/IPlatformStorageService.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Plpext.UI.Services;
|
||||||
|
|
||||||
|
public interface IPlatformStorageService
|
||||||
|
{
|
||||||
|
Task<string> GetOriginFilePath();
|
||||||
|
Task<string> GetTargetFolderPath();
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
86
src/Plpext/Plpext.UI/Styles/AudioPlayerControl.axaml
Normal file
86
src/Plpext/Plpext.UI/Styles/AudioPlayerControl.axaml
Normal 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>
|
15
src/Plpext/Plpext.UI/Styles/Border.axaml
Normal file
15
src/Plpext/Plpext.UI/Styles/Border.axaml
Normal 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>
|
30
src/Plpext/Plpext.UI/Styles/Button.axaml
Normal file
30
src/Plpext/Plpext.UI/Styles/Button.axaml
Normal 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>
|
14
src/Plpext/Plpext.UI/Styles/ProgressBar.axaml
Normal file
14
src/Plpext/Plpext.UI/Styles/ProgressBar.axaml
Normal 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>
|
15
src/Plpext/Plpext.UI/Styles/TextBox.axaml
Normal file
15
src/Plpext/Plpext.UI/Styles/TextBox.axaml
Normal 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>
|
9
src/Plpext/Plpext.UI/Styles/Window.axaml
Normal file
9
src/Plpext/Plpext.UI/Styles/Window.axaml
Normal 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>
|
120
src/Plpext/Plpext.UI/ViewModels/AudioPlayerViewModel.cs
Normal file
120
src/Plpext/Plpext.UI/ViewModels/AudioPlayerViewModel.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
129
src/Plpext/Plpext.UI/ViewModels/MainWindowViewModel.cs
Normal file
129
src/Plpext/Plpext.UI/ViewModels/MainWindowViewModel.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
187
src/Plpext/Plpext.UI/Views/MainWindow.axaml
Normal file
187
src/Plpext/Plpext.UI/Views/MainWindow.axaml
Normal 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>
|
12
src/Plpext/Plpext.UI/Views/MainWindow.axaml.cs
Normal file
12
src/Plpext/Plpext.UI/Views/MainWindow.axaml.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace Plpext.UI.Views
|
||||||
|
{
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
src/Plpext/Plpext.UI/app.manifest
Normal file
18
src/Plpext/Plpext.UI/app.manifest
Normal 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>
|
6
src/Plpext/Plpext.UI/velopack.json
Normal file
6
src/Plpext/Plpext.UI/velopack.json
Normal 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
62
src/Plpext/Plpext.sln
Normal 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
|
Loading…
Reference in a new issue