diff --git a/.gitignore b/.gitignore
index 9491a2f..8afdcb6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
\ No newline at end of file
+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
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..89dc443
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,6 @@
+
+
+ enable
+ 11.1.0
+
+
diff --git a/Harmonia.Core/Engine/BassAudioEngine.cs b/Harmonia.Core/Engine/BassAudioEngine.cs
index 237d38b..e8f5889 100644
--- a/Harmonia.Core/Engine/BassAudioEngine.cs
+++ b/Harmonia.Core/Engine/BassAudioEngine.cs
@@ -86,6 +86,8 @@ public class BassAudioEngine : IAudioEngine, IDisposable
public BassAudioEngine()
{
+ BassLoader.Initialize();
+
_mediaPlayer = new MediaPlayer();
_mediaPlayer.MediaLoaded += OnMediaLoaded;
_mediaPlayer.MediaFailed += OnPlaybackStopped;
diff --git a/Harmonia.Core/Engine/BassLoader.cs b/Harmonia.Core/Engine/BassLoader.cs
new file mode 100644
index 0000000..a856cc2
--- /dev/null
+++ b/Harmonia.Core/Engine/BassLoader.cs
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/Harmonia.Core/Harmonia.Core.csproj b/Harmonia.Core/Harmonia.Core.csproj
index 8719bc4..af2c18a 100644
--- a/Harmonia.Core/Harmonia.Core.csproj
+++ b/Harmonia.Core/Harmonia.Core.csproj
@@ -17,4 +17,19 @@
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
diff --git a/Harmonia.Core/Player/AudioPlayer.cs b/Harmonia.Core/Player/AudioPlayer.cs
index 3820ec6..e77a07b 100644
--- a/Harmonia.Core/Player/AudioPlayer.cs
+++ b/Harmonia.Core/Player/AudioPlayer.cs
@@ -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)
diff --git a/Harmonia.Core/Player/IAudioPlayer.cs b/Harmonia.Core/Player/IAudioPlayer.cs
index a5d2388..13bbcb4 100644
--- a/Harmonia.Core/Player/IAudioPlayer.cs
+++ b/Harmonia.Core/Player/IAudioPlayer.cs
@@ -24,5 +24,6 @@ public interface IAudioPlayer
Task PreviousAsync();
Task NextAsync();
+ event EventHandler PlayingSongChanged;
event PropertyChangedEventHandler PropertyChanged;
}
\ No newline at end of file
diff --git a/Harmonia.UI.Desktop/Harmonia.UI.Desktop.csproj b/Harmonia.UI.Desktop/Harmonia.UI.Desktop.csproj
new file mode 100644
index 0000000..00e8615
--- /dev/null
+++ b/Harmonia.UI.Desktop/Harmonia.UI.Desktop.csproj
@@ -0,0 +1,19 @@
+
+
+ WinExe
+
+ net9.0
+ enable
+ true
+ app.manifest
+
+
+
+
+
+
+
+
+
+
diff --git a/Harmonia.UI.Desktop/Program.cs b/Harmonia.UI.Desktop/Program.cs
new file mode 100644
index 0000000..5709de6
--- /dev/null
+++ b/Harmonia.UI.Desktop/Program.cs
@@ -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()
+ .UsePlatformDetect()
+ .WithInterFont()
+ .LogToTrace();
+
+}
diff --git a/Harmonia.UI.Desktop/app.manifest b/Harmonia.UI.Desktop/app.manifest
new file mode 100644
index 0000000..e0ce8d0
--- /dev/null
+++ b/Harmonia.UI.Desktop/app.manifest
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Harmonia.UI/App.axaml b/Harmonia.UI/App.axaml
new file mode 100644
index 0000000..28ae225
--- /dev/null
+++ b/Harmonia.UI/App.axaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Harmonia.UI/App.axaml.cs b/Harmonia.UI/App.axaml.cs
new file mode 100644
index 0000000..37b5c63
--- /dev/null
+++ b/Harmonia.UI/App.axaml.cs
@@ -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();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ 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();
+ }
+ else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
+ {
+ //singleViewPlatform.MainView = new MainView
+ //{
+ // DataContext = new MainViewModel()
+ //};
+
+ singleViewPlatform.MainView = ServiceProvider.GetRequiredService();
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+}
diff --git a/Harmonia.UI/Assets/avalonia-logo.ico b/Harmonia.UI/Assets/avalonia-logo.ico
new file mode 100644
index 0000000..da8d49f
Binary files /dev/null and b/Harmonia.UI/Assets/avalonia-logo.ico differ
diff --git a/Harmonia.UI/Harmonia.UI.csproj b/Harmonia.UI/Harmonia.UI.csproj
new file mode 100644
index 0000000..3e23391
--- /dev/null
+++ b/Harmonia.UI/Harmonia.UI.csproj
@@ -0,0 +1,33 @@
+
+
+ net9.0
+ enable
+ latest
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PlaybackBar.axaml
+
+
+
diff --git a/Harmonia.UI/ViewModels/MainViewModel.cs b/Harmonia.UI/ViewModels/MainViewModel.cs
new file mode 100644
index 0000000..4399bc1
--- /dev/null
+++ b/Harmonia.UI/ViewModels/MainViewModel.cs
@@ -0,0 +1,6 @@
+namespace Harmonia.UI.ViewModels;
+
+public partial class MainViewModel : ViewModelBase
+{
+ public string Greeting => "Welcome to Avalonia!";
+}
diff --git a/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs b/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs
new file mode 100644
index 0000000..a77d007
--- /dev/null
+++ b/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/Harmonia.UI/ViewModels/ViewModelBase.cs b/Harmonia.UI/ViewModels/ViewModelBase.cs
new file mode 100644
index 0000000..3d8b341
--- /dev/null
+++ b/Harmonia.UI/ViewModels/ViewModelBase.cs
@@ -0,0 +1,7 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Harmonia.UI.ViewModels;
+
+public class ViewModelBase : ObservableObject
+{
+}
diff --git a/Harmonia.UI/ViewModels/ViewModelLocator.cs b/Harmonia.UI/ViewModels/ViewModelLocator.cs
new file mode 100644
index 0000000..833666e
--- /dev/null
+++ b/Harmonia.UI/ViewModels/ViewModelLocator.cs
@@ -0,0 +1,12 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Harmonia.UI.ViewModels;
+
+public class ViewModelLocator
+{
+ public static MainViewModel MainViewModel
+ => App.ServiceProvider.GetRequiredService();
+
+ public static PlaybackBarViewModel PlaybackBarViewModel
+ => App.ServiceProvider.GetRequiredService();
+}
\ No newline at end of file
diff --git a/Harmonia.UI/Views/MainView.axaml b/Harmonia.UI/Views/MainView.axaml
new file mode 100644
index 0000000..8cf1808
--- /dev/null
+++ b/Harmonia.UI/Views/MainView.axaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Harmonia.UI/Views/MainView.axaml.cs b/Harmonia.UI/Views/MainView.axaml.cs
new file mode 100644
index 0000000..e68966f
--- /dev/null
+++ b/Harmonia.UI/Views/MainView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace Harmonia.UI.Views;
+
+public partial class MainView : UserControl
+{
+ public MainView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/Harmonia.UI/Views/MainWindow.axaml b/Harmonia.UI/Views/MainWindow.axaml
new file mode 100644
index 0000000..be3b2a6
--- /dev/null
+++ b/Harmonia.UI/Views/MainWindow.axaml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/Harmonia.UI/Views/MainWindow.axaml.cs b/Harmonia.UI/Views/MainWindow.axaml.cs
new file mode 100644
index 0000000..6347089
--- /dev/null
+++ b/Harmonia.UI/Views/MainWindow.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace Harmonia.UI.Views;
+
+public partial class MainWindow : Window
+{
+ public MainWindow()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/Harmonia.UI/Views/PlaybackBar.axaml b/Harmonia.UI/Views/PlaybackBar.axaml
new file mode 100644
index 0000000..55f9d9b
--- /dev/null
+++ b/Harmonia.UI/Views/PlaybackBar.axaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
diff --git a/Harmonia.UI/Views/PlaybackBar.axaml.cs b/Harmonia.UI/Views/PlaybackBar.axaml.cs
new file mode 100644
index 0000000..cb3e0b9
--- /dev/null
+++ b/Harmonia.UI/Views/PlaybackBar.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace Harmonia.UI.Views;
+
+public partial class PlaybackBar : UserControl
+{
+ public PlaybackBar()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/Harmonia.sln b/Harmonia.sln
index fcec17b..1042818 100644
--- a/Harmonia.sln
+++ b/Harmonia.sln
@@ -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