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