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