From bd9b30abbdfe0a681c73e7106bafea9c44d5b10a Mon Sep 17 00:00:00 2001 From: Brian Bicknell Date: Mon, 10 Mar 2025 09:50:05 -0400 Subject: [PATCH] Added playlist view and initial playlist view logic. --- Harmonia.Core/Player/AudioPlayer.cs | 2 + Harmonia.Core/Player/IAudioPlayer.cs | 1 + Harmonia.Tests/Harmonia.Tests.csproj | 2 +- Harmonia.UI/App.axaml | 46 +++++++++ Harmonia.UI/App.axaml.cs | 4 + Harmonia.UI/Caching/AudioBitmapCache.cs | 55 +++++++++++ Harmonia.UI/Caching/IAudioBitmapCache.cs | 10 ++ .../Converters/RepeatStateToIconConverter.cs | 28 ++++++ .../Converters/VolumeStateToIconConverter.cs | 37 ++++++++ Harmonia.UI/Harmonia.UI.csproj | 3 + Harmonia.UI/Models/VolumeState.cs | 10 ++ .../ViewModels/PlaybackBarViewModel.cs | 94 +++++++++++++++++++ Harmonia.UI/ViewModels/PlaylistViewModel.cs | 65 +++++++++++++ Harmonia.UI/ViewModels/ViewModelLocator.cs | 3 + Harmonia.UI/Views/PlaybackBar.axaml | 44 ++++++++- Harmonia.UI/Views/PlaybackBar.axaml.cs | 12 +++ Harmonia.UI/Views/PlayingSongInfo.axaml | 39 +++++--- Harmonia.UI/Views/PlaylistView.axaml | 93 ++++++++++++++++++ Harmonia.UI/Views/PlaylistView.axaml.cs | 91 ++++++++++++++++++ 19 files changed, 619 insertions(+), 20 deletions(-) create mode 100644 Harmonia.UI/Caching/AudioBitmapCache.cs create mode 100644 Harmonia.UI/Caching/IAudioBitmapCache.cs create mode 100644 Harmonia.UI/Converters/RepeatStateToIconConverter.cs create mode 100644 Harmonia.UI/Converters/VolumeStateToIconConverter.cs create mode 100644 Harmonia.UI/Models/VolumeState.cs create mode 100644 Harmonia.UI/ViewModels/PlaylistViewModel.cs create mode 100644 Harmonia.UI/Views/PlaylistView.axaml create mode 100644 Harmonia.UI/Views/PlaylistView.axaml.cs diff --git a/Harmonia.Core/Player/AudioPlayer.cs b/Harmonia.Core/Player/AudioPlayer.cs index e77a07b..c56c65d 100644 --- a/Harmonia.Core/Player/AudioPlayer.cs +++ b/Harmonia.Core/Player/AudioPlayer.cs @@ -20,6 +20,7 @@ public class AudioPlayer : IAudioPlayer { _playlist = value; NotifyPropertyChanged(nameof(Playlist)); + PlaylistChanged?.Invoke(this, new()); } } @@ -112,6 +113,7 @@ public class AudioPlayer : IAudioPlayer protected virtual int PreviousSongSecondsThreshold => 5; + public event EventHandler? PlaylistChanged; public event EventHandler? PlayingSongChanged; public event PropertyChangedEventHandler? PropertyChanged; diff --git a/Harmonia.Core/Player/IAudioPlayer.cs b/Harmonia.Core/Player/IAudioPlayer.cs index 13bbcb4..f71f2be 100644 --- a/Harmonia.Core/Player/IAudioPlayer.cs +++ b/Harmonia.Core/Player/IAudioPlayer.cs @@ -24,6 +24,7 @@ public interface IAudioPlayer Task PreviousAsync(); Task NextAsync(); + event EventHandler PlaylistChanged; event EventHandler PlayingSongChanged; event PropertyChangedEventHandler PropertyChanged; } \ No newline at end of file diff --git a/Harmonia.Tests/Harmonia.Tests.csproj b/Harmonia.Tests/Harmonia.Tests.csproj index 4085742..60f06c6 100644 --- a/Harmonia.Tests/Harmonia.Tests.csproj +++ b/Harmonia.Tests/Harmonia.Tests.csproj @@ -23,7 +23,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Harmonia.UI/App.axaml b/Harmonia.UI/App.axaml index 9e9aa24..fec9fb5 100644 --- a/Harmonia.UI/App.axaml +++ b/Harmonia.UI/App.axaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:Harmonia.UI.ViewModels" xmlns:control="clr-namespace:Harmonia.UI.Controls" + xmlns:converters="clr-namespace:Harmonia.UI.Converters" xmlns:semi="https://irihi.tech/semi" x:Class="Harmonia.UI.App" RequestedThemeVariant="Default"> @@ -16,6 +17,51 @@ + + + + + + + + + + + + M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06 + M13.854 5.646a.5.5 0 0 1 0 .708L12.207 8l1.647 1.646a.5.5 0 0 1-.708.708L11.5 8.707l-1.646 1.647a.5.5 0 0 1-.708-.708L10.793 8 9.146 6.354a.5.5 0 1 1 .708-.708L11.5 7.293l1.646-1.647a.5.5 0 0 1 .708 0 + + + M8.707 11.182A4.5 4.5 0 0 0 10.025 8a4.5 4.5 0 0 0-1.318-3.182L8 5.525A3.5 3.5 0 0 1 9.025 8 3.5 3.5 0 0 1 8 10.475z + M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06 + + + M8.707 11.182A4.5 4.5 0 0 0 10.025 8a4.5 4.5 0 0 0-1.318-3.182L8 5.525A3.5 3.5 0 0 1 9.025 8 3.5 3.5 0 0 1 8 10.475z + M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06 + + + M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.48 5.48 0 0 1 11.025 8a5.48 5.48 0 0 1-1.61 3.89z + M8.707 11.182A4.5 4.5 0 0 0 10.025 8a4.5 4.5 0 0 0-1.318-3.182L8 5.525A3.5 3.5 0 0 1 9.025 8 3.5 3.5 0 0 1 8 10.475z + M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06 + + + M11.536 14.01A8.47 8.47 0 0 0 14.026 8a8.47 8.47 0 0 0-2.49-6.01l-.708.707A7.48 7.48 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303z + M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.48 5.48 0 0 1 11.025 8a5.48 5.48 0 0 1-1.61 3.89z + M8.707 11.182A4.5 4.5 0 0 0 10.025 8a4.5 4.5 0 0 0-1.318-3.182L8 5.525A3.5 3.5 0 0 1 9.025 8 3.5 3.5 0 0 1 8 10.475z + M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06 + + + M0 3.5A.5.5 0 0 1 .5 3H1c2.202 0 3.827 1.24 4.874 2.418.49.552.865 1.102 1.126 1.532.26-.43.636-.98 1.126-1.532C9.173 4.24 10.798 3 13 3v1c-1.798 0-3.173 1.01-4.126 2.082A9.6 9.6 0 0 0 7.556 8a9.6 9.6 0 0 0 1.317 1.918C9.828 10.99 11.204 12 13 12v1c-2.202 0-3.827-1.24-4.874-2.418A10.6 10.6 0 0 1 7 9.05c-.26.43-.636.98-1.126 1.532C4.827 11.76 3.202 13 1 13H.5a.5.5 0 0 1 0-1H1c1.798 0 3.173-1.01 4.126-2.082A9.6 9.6 0 0 0 6.444 8a9.6 9.6 0 0 0-1.317-1.918C4.172 5.01 2.796 4 1 4H.5a.5.5 0 0 1-.5-.5 + M13 5.466V1.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192 + M13 14.466v-3.932a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192 + + + M11 5.466V4H5a4 4 0 0 0-3.584 5.777.5.5 0 1 1-.896.446A5 5 0 0 1 5 3h6V1.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192m3.81.086a.5.5 0 0 1 .67.225A5 5 0 0 1 11 13H5v1.466a.25.25 0 0 1-.41.192l-2.36-1.966a.25.25 0 0 1 0-.384l2.36-1.966a.25.25 0 0 1 .41.192V12h6a4 4 0 0 0 3.585-5.777.5.5 0 0 1 .225-.67Z + + + M11 4v1.466a.25.25 0 0 0 .41.192l2.36-1.966a.25.25 0 0 0 0-.384l-2.36-1.966a.25.25 0 0 0-.41.192V3H5a5 5 0 0 0-4.48 7.223.5.5 0 0 0 .896-.446A4 4 0 0 1 5 4zm4.48 1.777a.5.5 0 0 0-.896.446A4 4 0 0 1 11 12H5.001v-1.466a.25.25 0 0 0-.41-.192l-2.36 1.966a.25.25 0 0 0 0 .384l2.36 1.966a.25.25 0 0 0 .41-.192V13h6a5 5 0 0 0 4.48-7.223Z + M9 5.5a.5.5 0 0 0-.854-.354l-1.75 1.75a.5.5 0 1 0 .708.708L8 6.707V10.5a.5.5 0 0 0 1 0z + diff --git a/Harmonia.UI/App.axaml.cs b/Harmonia.UI/App.axaml.cs index 298ef19..e701f53 100644 --- a/Harmonia.UI/App.axaml.cs +++ b/Harmonia.UI/App.axaml.cs @@ -3,6 +3,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; using Harmonia.Core.Extensions; +using Harmonia.UI.Caching; using Harmonia.UI.ViewModels; using Harmonia.UI.Views; using Microsoft.Extensions.DependencyInjection; @@ -22,6 +23,9 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); services.AddHarmonia(); diff --git a/Harmonia.UI/Caching/AudioBitmapCache.cs b/Harmonia.UI/Caching/AudioBitmapCache.cs new file mode 100644 index 0000000..509466f --- /dev/null +++ b/Harmonia.UI/Caching/AudioBitmapCache.cs @@ -0,0 +1,55 @@ +using Avalonia.Media.Imaging; +using Harmonia.Core.Caching; +using Harmonia.Core.Imaging; +using Harmonia.Core.Models; +using Microsoft.Extensions.Caching.Memory; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Harmonia.UI.Caching; + +public class AudioBitmapCache(IAudioImageExtractor audioImageExtractor) : MemoryCache, IAudioBitmapCache +{ + protected override MemoryCacheOptions Options => new() + { + SizeLimit = 40, + CompactionPercentage = 0.2, + }; + + protected override TimeSpan SlidingExpiration => TimeSpan.FromSeconds(600); + + protected override int MaxConcurrentRequests => 8; + + protected override object? GetKey(Song key) + { + if (string.IsNullOrWhiteSpace(key.ImageHash) == false) + { + return key.ImageHash; + } + else if (string.IsNullOrWhiteSpace(key.ImageName) == false) + { + return key.ImageName; + } + + return null; + } + + protected override async ValueTask FetchAsync(Song key, CancellationToken cancellationToken) + { + SongPictureInfo? songPictureInfo = await audioImageExtractor.ExtractImageAsync(key.FileName, cancellationToken); + + if (songPictureInfo == null) + return null; + + using MemoryStream stream = new(songPictureInfo.Data); + + return new Bitmap(stream); + } + + protected override long GetEntrySize(Bitmap entry) + { + return 1; + } +} \ No newline at end of file diff --git a/Harmonia.UI/Caching/IAudioBitmapCache.cs b/Harmonia.UI/Caching/IAudioBitmapCache.cs new file mode 100644 index 0000000..28f5469 --- /dev/null +++ b/Harmonia.UI/Caching/IAudioBitmapCache.cs @@ -0,0 +1,10 @@ +using Avalonia.Media.Imaging; +using Harmonia.Core.Caching; +using Harmonia.Core.Models; + +namespace Harmonia.UI.Caching; + +public interface IAudioBitmapCache : ICache +{ + +} \ No newline at end of file diff --git a/Harmonia.UI/Converters/RepeatStateToIconConverter.cs b/Harmonia.UI/Converters/RepeatStateToIconConverter.cs new file mode 100644 index 0000000..f91099f --- /dev/null +++ b/Harmonia.UI/Converters/RepeatStateToIconConverter.cs @@ -0,0 +1,28 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data.Converters; +using Harmonia.Core.Player; +using System; +using System.Globalization; + +namespace Harmonia.UI.Converters; + +public class RepeatStateToIconConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not RepeatState repeatMode) + return null; + + string resourceName = repeatMode == RepeatState.RepeatOne + ? "RepeatOneIcon" + : "RepeatIcon"; + + return Application.Current?.FindResource(resourceName); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Harmonia.UI/Converters/VolumeStateToIconConverter.cs b/Harmonia.UI/Converters/VolumeStateToIconConverter.cs new file mode 100644 index 0000000..745ed4d --- /dev/null +++ b/Harmonia.UI/Converters/VolumeStateToIconConverter.cs @@ -0,0 +1,37 @@ +using Avalonia.Controls; +using Avalonia.Data.Converters; +using Avalonia; +using Harmonia.UI.Models; +using System; +using System.Globalization; +using Harmonia.Core.Playlists; + +namespace Harmonia.UI.Converters; + +public class VolumeStateToIconConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not VolumeState volumeState) + return AvaloniaProperty.UnsetValue; + + return Application.Current?.FindResource($"Volume{volumeState}Icon") ?? AvaloniaProperty.UnsetValue; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotImplementedException(); +} + +public class PlaylistSongEqualityConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is PlaylistSong playingSong && parameter is PlaylistSong currentSong) + { + return playingSong == currentSong; + } + + return false; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/Harmonia.UI/Harmonia.UI.csproj b/Harmonia.UI/Harmonia.UI.csproj index a06ad3d..fc1f7f0 100644 --- a/Harmonia.UI/Harmonia.UI.csproj +++ b/Harmonia.UI/Harmonia.UI.csproj @@ -27,6 +27,9 @@ + + PlaylistView.axaml + PlayingSongInfo.axaml diff --git a/Harmonia.UI/Models/VolumeState.cs b/Harmonia.UI/Models/VolumeState.cs new file mode 100644 index 0000000..a6f9c23 --- /dev/null +++ b/Harmonia.UI/Models/VolumeState.cs @@ -0,0 +1,10 @@ +namespace Harmonia.UI.Models; + +public enum VolumeState +{ + Muted, + Off, + Low, + Medium, + High +} \ No newline at end of file diff --git a/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs b/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs index d32cbf6..f359e9c 100644 --- a/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs +++ b/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs @@ -7,6 +7,7 @@ using Harmonia.Core.Models; using Harmonia.Core.Player; using Harmonia.Core.Playlists; using Harmonia.Core.Scanner; +using Harmonia.UI.Models; using System; using System.IO; using System.Linq; @@ -118,6 +119,57 @@ public partial class PlaybackBarViewModel : ViewModelBase, IDisposable } } + public double Volume + { + get + { + return _audioPlayer.Volume; + } + set + { + if (IsMuted) + IsMuted = false; + + _audioPlayer.Volume = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(VolumeState)); + } + } + + public bool IsMuted + { + get + { + return _audioPlayer.IsMuted; + } + set + { + _audioPlayer.IsMuted = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(VolumeState)); + } + } + + public VolumeState VolumeState + { + get + { + if (IsMuted) + return VolumeState.Muted; + + if (Volume == 0) + return VolumeState.Off; + + if (Volume < .33) + return VolumeState.Low; + + if (Volume < .66) + return VolumeState.Medium; + + return VolumeState.High; + } + } + public bool IsRandom { get @@ -131,11 +183,27 @@ public partial class PlaybackBarViewModel : ViewModelBase, IDisposable } } + public RepeatState RepeatState + { + get + { + return _audioPlayer.RepeatState; + } + private set + { + _audioPlayer.RepeatState = value; + OnPropertyChanged(); + } + } + public ICommand PlaySongCommand => new RelayCommand(Play); public ICommand PauseSongCommand => new RelayCommand(Pause); public ICommand StopSongCommand => new RelayCommand(Stop); public ICommand PreviousSongCommand => new RelayCommand(Previous); public ICommand NextSongCommand => new RelayCommand(Next); + public ICommand ToggleMuteCommand => new RelayCommand(ToggleMute); + public ICommand ToggleRandomizerCommand => new RelayCommand(ToggleRandomizer); + public ICommand ToggleRepeatCommand => new RelayCommand(ToggleRepeat); public PlaybackBarViewModel(IAudioPlayer audioPlayer, IAudioImageCache audioImageCache, IPlaylistRepository playlistRepository, IAudioFileScanner audioFileScanner) { @@ -219,6 +287,32 @@ public partial class PlaybackBarViewModel : ViewModelBase, IDisposable _audioPlayer.NextAsync(); } + public void ToggleMute() + { + IsMuted = !IsMuted; + } + + public void ToggleRandomizer() + { + IsRandom = !IsRandom; + } + + public void ToggleRepeat() + { + RepeatState = GetNextRepeatState(); + } + + private RepeatState GetNextRepeatState() + { + return _audioPlayer.RepeatState switch + { + RepeatState.Off => RepeatState.RepeatAll, + RepeatState.RepeatAll => RepeatState.RepeatOne, + RepeatState.RepeatOne => RepeatState.Off, + _ => _audioPlayer.RepeatState, + }; + } + public void Dispose() { SongImageSource?.Dispose(); diff --git a/Harmonia.UI/ViewModels/PlaylistViewModel.cs b/Harmonia.UI/ViewModels/PlaylistViewModel.cs new file mode 100644 index 0000000..93dcddb --- /dev/null +++ b/Harmonia.UI/ViewModels/PlaylistViewModel.cs @@ -0,0 +1,65 @@ +using Harmonia.Core.Player; +using Harmonia.Core.Playlists; +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using System.Threading; +using Avalonia.Media.Imaging; +using System.Collections.Concurrent; +using Harmonia.UI.Caching; + +namespace Harmonia.UI.ViewModels; + +public class PlaylistViewModel : ViewModelBase +{ + private readonly IAudioPlayer _audioPlayer; + private readonly IAudioBitmapCache _audioBitmapImageCache; + private readonly ConcurrentDictionary _bitmapDictionary = []; + + public PlaylistSong? PlayingSong => _audioPlayer.PlayingSong; + + private ObservableCollection _playlistSongs = []; + public ObservableCollection PlaylistSongs + { + get + { + return _playlistSongs; + } + set + { + _playlistSongs = value; + OnPropertyChanged(); + } + } + + public PlaylistViewModel(IAudioPlayer audioPlayer, IAudioBitmapCache audioBitmapImageCache) + { + _audioPlayer = audioPlayer; + _audioPlayer.PlaylistChanged += OnAudioPlayerPlaylistChanged; + _audioPlayer.PlayingSongChanged += OnAudioPlayerPlayingSongChanged; + + _audioBitmapImageCache = audioBitmapImageCache; + } + + private void OnAudioPlayerPlaylistChanged(object? sender, EventArgs e) + { + PlaylistSong[] playlistSongs = _audioPlayer.Playlist?.Songs.ToArray() ?? []; + + PlaylistSongs = [.. playlistSongs]; + } + + private void OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e) + { + OnPropertyChanged(nameof(PlayingSong)); + } + + public void PlaySong(PlaylistSong playlistSong) + { + _audioPlayer.LoadAsync(playlistSong, PlaybackMode.LoadAndPlay); + } + + public async Task GetBitmapAsync(PlaylistSong playlistSong, CancellationToken cancellationToken) + { + return await _audioBitmapImageCache.GetAsync(playlistSong.Song, cancellationToken); + } +} \ No newline at end of file diff --git a/Harmonia.UI/ViewModels/ViewModelLocator.cs b/Harmonia.UI/ViewModels/ViewModelLocator.cs index 92e0f9a..5cf0d07 100644 --- a/Harmonia.UI/ViewModels/ViewModelLocator.cs +++ b/Harmonia.UI/ViewModels/ViewModelLocator.cs @@ -12,4 +12,7 @@ public class ViewModelLocator public static PlayingSongInfoViewModel PlayingSongInfoViewModel => App.ServiceProvider.GetRequiredService(); + + public static PlaylistViewModel PlaylistViewModel + => App.ServiceProvider.GetRequiredService(); } \ No newline at end of file diff --git a/Harmonia.UI/Views/PlaybackBar.axaml b/Harmonia.UI/Views/PlaybackBar.axaml index f02395b..6b4db18 100644 --- a/Harmonia.UI/Views/PlaybackBar.axaml +++ b/Harmonia.UI/Views/PlaybackBar.axaml @@ -23,6 +23,12 @@ + + + + + + + + + + diff --git a/Harmonia.UI/Views/PlaybackBar.axaml.cs b/Harmonia.UI/Views/PlaybackBar.axaml.cs index aec8b2d..ba07df9 100644 --- a/Harmonia.UI/Views/PlaybackBar.axaml.cs +++ b/Harmonia.UI/Views/PlaybackBar.axaml.cs @@ -45,4 +45,16 @@ public partial class PlaybackBar : UserControl _viewModel.CurrentPosition = slider.Value; _viewModel.IsPositionChangeInProgress = false; } + + private void VolumeSlider_PointerWheelChanged(object? sender, PointerWheelEventArgs e) + { + double mouseWheelDelta = e.Delta.Y; + + if (mouseWheelDelta == 0) + return; + + double delta = mouseWheelDelta > 0 ? .02 : -.02; + + _viewModel.Volume += delta; + } } \ No newline at end of file diff --git a/Harmonia.UI/Views/PlayingSongInfo.axaml b/Harmonia.UI/Views/PlayingSongInfo.axaml index 633135c..3653b24 100644 --- a/Harmonia.UI/Views/PlayingSongInfo.axaml +++ b/Harmonia.UI/Views/PlayingSongInfo.axaml @@ -3,25 +3,36 @@ 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" xmlns:converter="clr-namespace:Harmonia.UI.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" DataContext="{x:Static vm:ViewModelLocator.PlayingSongInfoViewModel}" x:Class="Harmonia.UI.Views.PlayingSongInfo" x:DataType="vm:PlayingSongInfoViewModel"> - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Harmonia.UI/Views/PlaylistView.axaml b/Harmonia.UI/Views/PlaylistView.axaml new file mode 100644 index 0000000..0e9c52c --- /dev/null +++ b/Harmonia.UI/Views/PlaylistView.axaml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Harmonia.UI/Views/PlaylistView.axaml.cs b/Harmonia.UI/Views/PlaylistView.axaml.cs new file mode 100644 index 0000000..c012b35 --- /dev/null +++ b/Harmonia.UI/Views/PlaylistView.axaml.cs @@ -0,0 +1,91 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media.Imaging; +using Avalonia.Threading; +using Harmonia.Core.Caching; +using Harmonia.Core.Imaging; +using Harmonia.Core.Playlists; +using Harmonia.UI.ViewModels; +using Microsoft.Extensions.Caching.Memory; +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Harmonia.UI.Views; + +public partial class PlaylistView : UserControl +{ + private readonly PlaylistViewModel _viewModel; + private readonly ConcurrentDictionary _imageCancellationTokens = []; + + public PlaylistView() + { + InitializeComponent(); + _viewModel = (PlaylistViewModel)DataContext!; + } + + private void ListBox_DoubleTapped(object? sender, TappedEventArgs e) + { + if (sender is ListBox listBox && listBox.SelectedItem is PlaylistSong playlistSong) + { + _viewModel.PlaySong(playlistSong); + } + } + + private void Image_Loaded(object? sender, RoutedEventArgs e) + { + if (sender is not Image image) + return; + + if (image.DataContext is not PlaylistSong playlistSong) + return; + + Task.Run(() => DoSomethingAsync(image, playlistSong)); + } + + private async Task DoSomethingAsync(Image image, PlaylistSong playlistSong) + { + int hashCode = image.GetHashCode(); + + _imageCancellationTokens.TryGetValue(hashCode, out CancellationTokenSource? cancellationTokenSource); + cancellationTokenSource?.Cancel(); + + cancellationTokenSource = new(); + + Bitmap? bitmap = await _viewModel.GetBitmapAsync(playlistSong, cancellationTokenSource.Token); + + if (bitmap == null) + return; + + await Dispatcher.UIThread.InvokeAsync(() => SetSongImageSource(image, bitmap)); + } + + private static void SetSongImageSource(Image image, Bitmap bitmap) + { + image.Source = bitmap; + } + + private void Image_Unloaded(object? sender, RoutedEventArgs e) + { + if (sender is not Image image) + return; + + if (image.DataContext is not PlaylistSong playlistSong) + return; + + if (image.Source is Bitmap bitmap) + { + bitmap.Dispose(); + } + + image.Source = null; + + int hashCode = image.GetHashCode(); + + _imageCancellationTokens.TryGetValue(hashCode, out CancellationTokenSource? cancellationTokenSource); + cancellationTokenSource?.Cancel(); + } +} \ No newline at end of file