Added AvaloniaUI project.

This commit is contained in:
2025-03-01 17:03:06 -05:00
parent f78fe6fa2b
commit fc28004c89
25 changed files with 616 additions and 2 deletions

95
.gitignore vendored
View File

@@ -29,7 +29,6 @@ x86/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
@@ -63,6 +62,9 @@ project.lock.json
project.fragment.lock.json
artifacts/
# Tye
.tye/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
@@ -360,4 +362,93 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd
##
## Visual studio for Mac
##
# globs
Makefile.in
*.userprefs
*.usertasks
config.make
config.status
aclocal.m4
install-sh
autom4te.cache/
*.tar.gz
tarballs/
test-results/
# Mac bundle stuff
*.dmg
*.app
# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# JetBrains Rider
.idea/
*.sln.iml
##
## Visual Studio Code
##
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

6
Directory.Build.props Normal file
View File

@@ -0,0 +1,6 @@
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<AvaloniaVersion>11.1.0</AvaloniaVersion>
</PropertyGroup>
</Project>

View File

@@ -86,6 +86,8 @@ public class BassAudioEngine : IAudioEngine, IDisposable
public BassAudioEngine()
{
BassLoader.Initialize();
_mediaPlayer = new MediaPlayer();
_mediaPlayer.MediaLoaded += OnMediaLoaded;
_mediaPlayer.MediaFailed += OnPlaybackStopped;

View File

@@ -0,0 +1,22 @@
using System.Runtime.InteropServices;
namespace Harmonia.Core.Engine;
public static class BassLoader
{
public static void Initialize()
{
string archFolder = Environment.Is64BitProcess ? "x64" : "x86";
string bassPluginPath = Path.Combine("Plugins", "Bass", "Win32", archFolder);
LoadLibrary(bassPluginPath, "bass.dll");
LoadLibrary(bassPluginPath, "bassflac.dll");
}
private static void LoadLibrary(string bassPluginPath, string resourceName)
{
string resourcePath = Path.Combine(bassPluginPath, resourceName);
NativeLibrary.Load(resourcePath);
}
}

View File

@@ -17,4 +17,19 @@
<PackageReference Include="TagLibSharp" Version="2.3.0" />
</ItemGroup>
<ItemGroup>
<None Update="Plugins\Bass\Win32\x64\bass.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Plugins\Bass\Win32\x64\bassflac.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Plugins\Bass\Win32\x86\bass.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Plugins\Bass\Win32\x86\bassflac.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -36,6 +36,7 @@ public class AudioPlayer : IAudioPlayer
{
_playingSong = value;
NotifyPropertyChanged(nameof(PlayingSong));
PlayingSongChanged?.Invoke(this, new());
}
}
@@ -111,6 +112,7 @@ public class AudioPlayer : IAudioPlayer
protected virtual int PreviousSongSecondsThreshold => 5;
public event EventHandler? PlayingSongChanged;
public event PropertyChangedEventHandler? PropertyChanged;
public AudioPlayer(IAudioEngine audioEngine, IPlaylistRepository playlistRepository)

View File

@@ -24,5 +24,6 @@ public interface IAudioPlayer
Task PreviousAsync();
Task NextAsync();
event EventHandler PlayingSongChanged;
event PropertyChangedEventHandler PropertyChanged;
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<!--If you are willing to use Windows/MacOS native APIs you will need to create 3 projects.
One for Windows with net8.0-windows TFM, one for MacOS with net8.0-macos and one with net8.0 TFM for Linux.-->
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Desktop" Version="11.2.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Harmonia.UI\Harmonia.UI.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,23 @@
using System;
using Avalonia;
namespace Harmonia.UI.Desktop;
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) => 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 @@
<?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 embeded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="AvaloniaTest.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>

18
Harmonia.UI/App.axaml Normal file
View File

@@ -0,0 +1,18 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Harmonia.UI.ViewModels"
x:Class="Harmonia.UI.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<FluentTheme />
</Application.Styles>
<Application.Resources>
<ResourceDictionary>
<vm:ViewModelLocator x:Key="Locator" />
</ResourceDictionary>
</Application.Resources>
</Application>

62
Harmonia.UI/App.axaml.cs Normal file
View File

@@ -0,0 +1,62 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using Harmonia.Core.Extensions;
using Harmonia.UI.ViewModels;
using Harmonia.UI.Views;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace Harmonia.UI;
public partial class App : Application
{
public static IServiceProvider ServiceProvider { get; private set; }
static App()
{
ServiceCollection services = new();
services.AddSingleton<MainViewModel>();
services.AddSingleton<MainWindow>();
services.AddSingleton<PlaybackBarViewModel>();
services.AddHarmonia();
ServiceProvider = services.BuildServiceProvider();
}
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
// Line below is needed to remove Avalonia data validation.
// Without this line you will get duplicate validations from both Avalonia and CT
BindingPlugins.DataValidators.RemoveAt(0);
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
//desktop.MainWindow = new MainWindow
//{
// DataContext = new MainViewModel()
//};
desktop.MainWindow = ServiceProvider.GetRequiredService<MainWindow>();
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
//singleViewPlatform.MainView = new MainView
//{
// DataContext = new MainViewModel()
//};
singleViewPlatform.MainView = ServiceProvider.GetRequiredService<MainWindow>();
}
base.OnFrameworkInitializationCompleted();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersion)" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.0" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Harmonia.Core\Harmonia.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Views\PlaybackBar.axaml.cs">
<DependentUpon>PlaybackBar.axaml</DependentUpon>
</Compile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
namespace Harmonia.UI.ViewModels;
public partial class MainViewModel : ViewModelBase
{
public string Greeting => "Welcome to Avalonia!";
}

View File

@@ -0,0 +1,178 @@
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using Harmonia.Core.Caching;
using Harmonia.Core.Imaging;
using Harmonia.Core.Models;
using Harmonia.Core.Player;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Harmonia.UI.ViewModels;
public partial class PlaybackBarViewModel : ViewModelBase
{
private readonly IAudioPlayer _audioPlayer;
private readonly IAudioImageCache _audioImageCache;
private bool _isPositionChangeInProgress;
private CancellationTokenSource? _audioImageCancellationTokenSource;
private Song? _song;
public Song? Song
{
get
{
return _song;
}
private set
{
_song = value;
OnPropertyChanged();
CurrentPosition = 0; // What if player is being loaded and returning to save state?
Position = 0;
MaxPosition = value?.Length.TotalSeconds ?? 0;
}
}
private Bitmap? _songImageSource;
public Bitmap? SongImageSource
{
get
{
return _songImageSource;
}
private set
{
_songImageSource = value;
OnPropertyChanged();
}
}
private double _currentPosition;
public double CurrentPosition
{
get
{
return _currentPosition;
}
set
{
_currentPosition = value;
OnPropertyChanged();
if (_isPositionChangeInProgress)
_audioPlayer.Position = value;
}
}
private double _position;
public double Position
{
get
{
return _position;
}
private set
{
_position = value;
OnPropertyChanged();
}
}
private double _maxPosition;
public double MaxPosition
{
get
{
return _maxPosition;
}
set
{
_maxPosition = value;
OnPropertyChanged();
}
}
public bool IsRandom
{
get
{
return _audioPlayer.IsRandom;
}
private set
{
_audioPlayer.IsRandom = value;
OnPropertyChanged();
}
}
public string Greeting => "Welcome to Harmonia!";
public PlaybackBarViewModel(IAudioPlayer audioPlayer, IAudioImageCache audioImageCache)
{
_audioPlayer = audioPlayer;
_audioPlayer.PlayingSongChanged += OnAudioPlayerPlayingSongChanged;
_audioImageCache = audioImageCache;
}
private void OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e)
{
Song = _audioPlayer.PlayingSong?.Song;
Task.Run(UpdateImage);
}
private async Task UpdateImage()
{
// TODO: Show default picture
if (Song == null)
return;
if (_audioImageCancellationTokenSource != null)
await _audioImageCancellationTokenSource.CancelAsync();
_audioImageCancellationTokenSource = new();
CancellationToken cancellationToken = _audioImageCancellationTokenSource.Token;
SongPictureInfo? songPictureInfo = await _audioImageCache.GetAsync(Song, cancellationToken);
if (songPictureInfo == null)
return;
await Dispatcher.UIThread.InvokeAsync(() => SetSongImageSource(songPictureInfo));
}
private void SetSongImageSource(SongPictureInfo songPictureInfo)
{
SongImageSource = new(songPictureInfo.Stream);
}
public void Play()
{
_audioPlayer.Play();
}
public void Pause()
{
_audioPlayer.Pause();
}
public void Stop()
{
_audioPlayer.Stop();
CurrentPosition = 0;
Position = 0;
}
public void Previous()
{
_audioPlayer.PreviousAsync();
}
public void Next()
{
_audioPlayer.NextAsync();
}
}

View File

@@ -0,0 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Harmonia.UI.ViewModels;
public class ViewModelBase : ObservableObject
{
}

View File

@@ -0,0 +1,12 @@
using Microsoft.Extensions.DependencyInjection;
namespace Harmonia.UI.ViewModels;
public class ViewModelLocator
{
public static MainViewModel MainViewModel
=> App.ServiceProvider.GetRequiredService<MainViewModel>();
public static PlaybackBarViewModel PlaybackBarViewModel
=> App.ServiceProvider.GetRequiredService<PlaybackBarViewModel>();
}

View File

@@ -0,0 +1,22 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Harmonia.UI.ViewModels"
xmlns:views="clr-namespace:Harmonia.UI.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
DataContext="{x:Static vm:ViewModelLocator.MainViewModel}"
x:Class="Harmonia.UI.Views.MainView"
x:DataType="vm:MainViewModel">
<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:MainViewModel />
</Design.DataContext>
<StackPanel>
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<views:PlaybackBar></views:PlaybackBar>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Harmonia.UI.Views;
public partial class MainView : UserControl
{
public MainView()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,12 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Harmonia.UI.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:Harmonia.UI.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Harmonia.UI.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico"
Title="Harmonia.UI">
<views:MainView />
</Window>

View File

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

View File

@@ -0,0 +1,17 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Harmonia.UI.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
DataContext="{x:Static vm:ViewModelLocator.PlaybackBarViewModel}"
x:Class="Harmonia.UI.Views.PlaybackBar"
x:DataType="vm:PlaybackBarViewModel">
<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:MainViewModel />
</Design.DataContext>
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Harmonia.UI.Views;
public partial class PlaybackBar : UserControl
{
public PlaybackBar()
{
InitializeComponent();
}
}

View File

@@ -7,6 +7,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Harmonia.Core", "Harmonia.C
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Harmonia.Tests", "Harmonia.Tests\Harmonia.Tests.csproj", "{F17DDB67-F609-4316-B0AB-2235BE69135B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Harmonia.UI", "Harmonia.UI\Harmonia.UI.csproj", "{3277DC9A-6AC5-6B0A-6A1A-A64ABB7660AE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Harmonia.UI.Desktop", "Harmonia.UI.Desktop\Harmonia.UI.Desktop.csproj", "{61B86E6A-A7E9-4946-E488-40A7E12ED4FC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -21,8 +25,19 @@ Global
{F17DDB67-F609-4316-B0AB-2235BE69135B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F17DDB67-F609-4316-B0AB-2235BE69135B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F17DDB67-F609-4316-B0AB-2235BE69135B}.Release|Any CPU.Build.0 = Release|Any CPU
{3277DC9A-6AC5-6B0A-6A1A-A64ABB7660AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3277DC9A-6AC5-6B0A-6A1A-A64ABB7660AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3277DC9A-6AC5-6B0A-6A1A-A64ABB7660AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3277DC9A-6AC5-6B0A-6A1A-A64ABB7660AE}.Release|Any CPU.Build.0 = Release|Any CPU
{61B86E6A-A7E9-4946-E488-40A7E12ED4FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{61B86E6A-A7E9-4946-E488-40A7E12ED4FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{61B86E6A-A7E9-4946-E488-40A7E12ED4FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{61B86E6A-A7E9-4946-E488-40A7E12ED4FC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8E2FCA09-C9FA-43C8-B6FB-002DC4C264EB}
EndGlobalSection
EndGlobal