diff --git a/Harmonia.Core/Caching/MemoryCache.cs b/Harmonia.Core/Caching/MemoryCache.cs index ea03fc6..0a7ff78 100644 --- a/Harmonia.Core/Caching/MemoryCache.cs +++ b/Harmonia.Core/Caching/MemoryCache.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; namespace Harmonia.Core.Caching; @@ -27,10 +28,16 @@ public abstract class MemoryCache : Cache where TKey var cacheEntryOptions = new MemoryCacheEntryOptions() .SetSize(entrySize) - .SetSlidingExpiration(SlidingExpiration); + .SetSlidingExpiration(SlidingExpiration) + .RegisterPostEvictionCallback(PostEvictionCallback); _memoryCache.Set(key, entry, cacheEntryOptions); } + protected virtual void PostEvictionCallback(object? cacheKey, object? cacheValue, EvictionReason evictionReason, object? state) + { + + } + protected abstract long GetEntrySize(TValue entry); } \ No newline at end of file diff --git a/Harmonia.Core/Harmonia.Core.csproj b/Harmonia.Core/Harmonia.Core.csproj index 28fc718..5b4473d 100644 --- a/Harmonia.Core/Harmonia.Core.csproj +++ b/Harmonia.Core/Harmonia.Core.csproj @@ -7,12 +7,12 @@ - - - - - - + + + + + + diff --git a/Harmonia.Core/Models/Song.cs b/Harmonia.Core/Models/Song.cs index 50b5efd..c90c18d 100644 --- a/Harmonia.Core/Models/Song.cs +++ b/Harmonia.Core/Models/Song.cs @@ -23,4 +23,26 @@ public class Song public string? FileDirectory => Directory.GetParent(FileName)?.Name; public string? FileType => Path.GetExtension(FileName)?.Replace(".", "").ToUpper(); public string ShortFileName => Path.GetFileNameWithoutExtension(FileName); + + public void Update(Song song) + { + if (string.Equals(song.FileName, FileName, StringComparison.OrdinalIgnoreCase) == false) + return; + + Size = song.Size; + LastModified = song.LastModified; + Title = song.Title; + Album = song.Album; + Artists = song.Artists; + AlbumArtists = song.AlbumArtists; + DiscNumber = song.DiscNumber; + TrackNumber = song.TrackNumber; + Length = song.Length; + Year = song.Year; + Genre = song.Genre; + BitRate = song.BitRate; + SampleRate = song.SampleRate; + ImageName = song?.ImageName; + ImageHash = song?.ImageHash; + } } \ No newline at end of file diff --git a/Harmonia.Core/Playlists/Playlist.cs b/Harmonia.Core/Playlists/Playlist.cs index 2277d98..dc027b7 100644 --- a/Harmonia.Core/Playlists/Playlist.cs +++ b/Harmonia.Core/Playlists/Playlist.cs @@ -172,6 +172,7 @@ public class Playlist foreach (PlaylistSong playlistSong in playlistSongs) { //playlistSong.Song = song; + //playlistSong.Song.Update(song); } } } diff --git a/Harmonia.Tests/Harmonia.Tests.csproj b/Harmonia.Tests/Harmonia.Tests.csproj index 60f06c6..96bbe8b 100644 --- a/Harmonia.Tests/Harmonia.Tests.csproj +++ b/Harmonia.Tests/Harmonia.Tests.csproj @@ -16,14 +16,14 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Harmonia.UI.Desktop/Harmonia.UI.Desktop.csproj b/Harmonia.UI.Desktop/Harmonia.UI.Desktop.csproj index 4a72ae2..aeb44be 100644 --- a/Harmonia.UI.Desktop/Harmonia.UI.Desktop.csproj +++ b/Harmonia.UI.Desktop/Harmonia.UI.Desktop.csproj @@ -10,7 +10,7 @@ - + diff --git a/Harmonia.UI/Harmonia.UI.csproj b/Harmonia.UI/Harmonia.UI.csproj index bbb2fc3..d005fff 100644 --- a/Harmonia.UI/Harmonia.UI.csproj +++ b/Harmonia.UI/Harmonia.UI.csproj @@ -11,15 +11,15 @@ - - - + + + - - - + + + diff --git a/Harmonia.WinUI/Caching/AudioBitmapImageCache.cs b/Harmonia.WinUI/Caching/AudioBitmapImageCache.cs index 6825b22..9a21d2b 100644 --- a/Harmonia.WinUI/Caching/AudioBitmapImageCache.cs +++ b/Harmonia.WinUI/Caching/AudioBitmapImageCache.cs @@ -2,8 +2,10 @@ using Harmonia.Core.Imaging; using Harmonia.Core.Models; using Microsoft.Extensions.Caching.Memory; +using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml.Media.Imaging; using System; +using System.Collections.Concurrent; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -99,4 +101,128 @@ public class AudioBitmapImageCache(IAudioImageExtractor audioImageExtractor) : M { return entry.DecodePixelWidth * entry.DecodePixelHeight; } +} + + +public class AudioBitmapImageCache2 : MemoryCache, IAudioBitmapImageCache +{ + private readonly IAudioImageExtractor _audioImageExtractor; + private readonly BitmapImage[] _bitmapPool; + private int _nextIndex = 0; + private ConcurrentDictionary _test = []; + + protected override MemoryCacheOptions Options => new() + { + SizeLimit = 200_000_000, + CompactionPercentage = 0.2, + }; + + protected override TimeSpan SlidingExpiration => TimeSpan.FromSeconds(600); + protected override int MaxConcurrentRequests => 8; + protected virtual int MaxImageWidthOrHeight => 1000; + protected virtual int BitmapPoolSize => 64; + + public AudioBitmapImageCache2(IAudioImageExtractor audioImageExtractor) + { + _audioImageExtractor = audioImageExtractor; + _bitmapPool = new BitmapImage[BitmapPoolSize]; + + InitializeBitmapPool(); + } + + private void InitializeBitmapPool() + { + for (int i = 0; i < _bitmapPool.Length; i++) + _bitmapPool[i] = new BitmapImage(); + + //DispatcherQueue dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + + //TaskCompletionSource taskCompletionSource = new(); + + //dispatcherQueue.TryEnqueue(() => + //{ + // for (int i = 0; i < _bitmapPool.Length; i++) + // _bitmapPool[i] = new BitmapImage(); + + // taskCompletionSource.SetResult(); + //}); + + //taskCompletionSource.Task.GetAwaiter().GetResult(); + } + + 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 "Default"; + } + + protected override async ValueTask FetchAsync(Song key, CancellationToken cancellationToken) + { + int index = Interlocked.Increment(ref _nextIndex); + BitmapImage bitmapImage = _bitmapPool[index % _bitmapPool.Length]; + //_test.AddOrUpdate(index, bitmapImage); + + SongPictureInfo? songPictureInfo = await _audioImageExtractor.ExtractImageAsync(key.FileName, cancellationToken); + + if (songPictureInfo == null) + { + bitmapImage.UriSource = new("ms-appx:///Assets/Default.png", UriKind.Absolute); + } + else + { + using MemoryStream stream = new(songPictureInfo.Data); + await bitmapImage.SetSourceAsync(stream.AsRandomAccessStream()); + } + + bitmapImage.DecodePixelWidth = GetDecodePixelWidth(bitmapImage); + bitmapImage.DecodePixelHeight = GetDecodePixelHeight(bitmapImage); + + return bitmapImage; + } + + private int GetDecodePixelWidth(BitmapImage bitmapImage) + { + int originalImageWidth = bitmapImage.PixelWidth; + int orignalImageHeight = bitmapImage.PixelHeight; + + if (originalImageWidth <= MaxImageWidthOrHeight && orignalImageHeight <= MaxImageWidthOrHeight) + return 0; + + if (orignalImageHeight > originalImageWidth) + return 0; + + return MaxImageWidthOrHeight; + } + + private int GetDecodePixelHeight(BitmapImage bitmapImage) + { + int originalImageWidth = bitmapImage.PixelWidth; + int orignalImageHeight = bitmapImage.PixelHeight; + + if (originalImageWidth <= MaxImageWidthOrHeight && orignalImageHeight <= MaxImageWidthOrHeight) + return 0; + + if (originalImageWidth > orignalImageHeight) + return 0; + + return MaxImageWidthOrHeight; + } + + protected override long GetEntrySize(BitmapImage entry) + { + return entry.DecodePixelWidth * entry.DecodePixelHeight; + } + + protected override void PostEvictionCallback(object? cacheKey, object? cacheValue, EvictionReason evictionReason, object? state) + { + + } } \ No newline at end of file diff --git a/Harmonia.WinUI/Flex/IFlexView.cs b/Harmonia.WinUI/Flex/IFlexView.cs new file mode 100644 index 0000000..a6109b7 --- /dev/null +++ b/Harmonia.WinUI/Flex/IFlexView.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Harmonia.WinUI.Flex; + +public interface IFlexView +{ + FlexLayout Layout { get; } + int ImageWidth { get; } + FlexOrientation ListOrientation { get; } + int RowSpacing { get; } + int ColumnSpacing { get; } + int TextLineHeight { get; } + FlexOrientation SubtitleFooterOrientation { get; } + int SubtitleFooterMargin { get; } + FlexOrientation ImageToTextOrientation { get; } + int TitleFontSize { get; } + int SubtitleFontSize { get; } + int FooterFontSize { get; } +} + +public abstract class GridFlexViewBase : IFlexView +{ + public FlexLayout Layout => FlexLayout.Grid; + public FlexOrientation ListOrientation => FlexOrientation.Horizontal; + public FlexOrientation SubtitleFooterOrientation => FlexOrientation.Vertical; + public FlexOrientation ImageToTextOrientation => FlexOrientation.Vertical; + + public abstract int ImageWidth { get; } + public abstract int RowSpacing { get; } + public abstract int ColumnSpacing { get; } + public abstract int TextLineHeight { get; } + public abstract int SubtitleFooterMargin { get; } + public abstract int TitleFontSize { get; } + public abstract int SubtitleFontSize { get; } + public abstract int FooterFontSize { get; } +} + +public abstract class ListFlexViewBase : IFlexView +{ + public FlexLayout Layout => FlexLayout.List; + public FlexOrientation ListOrientation => FlexOrientation.Vertical; + public FlexOrientation SubtitleFooterOrientation => FlexOrientation.Vertical; + public FlexOrientation ImageToTextOrientation => FlexOrientation.Vertical; + + public abstract int ImageWidth { get; } + + public abstract int RowSpacing { get; } + + public abstract int ColumnSpacing { get; } + + public abstract int TextLineHeight { get; } + + public abstract int SubtitleFooterMargin { get; } + + public abstract int TitleFontSize { get; } + + public abstract int SubtitleFontSize { get; } + + public abstract int FooterFontSize { get; } +} + +public enum FlexLayout +{ + List, + Grid +} + +public enum FlexOrientation +{ + Horizontal, + Vertical +} \ No newline at end of file diff --git a/Harmonia.WinUI/Harmonia.WinUI.csproj b/Harmonia.WinUI/Harmonia.WinUI.csproj index 02c9142..5ab11a3 100644 --- a/Harmonia.WinUI/Harmonia.WinUI.csproj +++ b/Harmonia.WinUI/Harmonia.WinUI.csproj @@ -43,9 +43,9 @@ - - - + + + diff --git a/Harmonia.WinUI/Resources/Styles.xaml b/Harmonia.WinUI/Resources/Styles.xaml index 8b976d2..58152f6 100644 --- a/Harmonia.WinUI/Resources/Styles.xaml +++ b/Harmonia.WinUI/Resources/Styles.xaml @@ -5,19 +5,19 @@ diff --git a/Harmonia.WinUI/ViewModels/PlayingSongViewModel.cs b/Harmonia.WinUI/ViewModels/PlayingSongViewModel.cs index a5c8ee6..653e105 100644 --- a/Harmonia.WinUI/ViewModels/PlayingSongViewModel.cs +++ b/Harmonia.WinUI/ViewModels/PlayingSongViewModel.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; namespace Harmonia.WinUI.ViewModels; -public class PlayingSongViewModel : ViewModelBase +public partial class PlayingSongViewModel : ViewModelBase { private readonly IAudioPlayer _audioPlayer; private readonly IAudioBitmapImageCache _audioBitmapImageCache; diff --git a/Harmonia.WinUI/ViewModels/PlaylistViewModel.cs b/Harmonia.WinUI/ViewModels/PlaylistViewModel.cs index d78d6ff..b01a0ed 100644 --- a/Harmonia.WinUI/ViewModels/PlaylistViewModel.cs +++ b/Harmonia.WinUI/ViewModels/PlaylistViewModel.cs @@ -1,5 +1,7 @@ using CommunityToolkit.Mvvm.Input; +using Harmonia.Core.Caching; using Harmonia.Core.Engine; +using Harmonia.Core.Imaging; using Harmonia.Core.Models; using Harmonia.Core.Player; using Harmonia.Core.Playlists; @@ -8,10 +10,10 @@ using Harmonia.WinUI.Caching; using Harmonia.WinUI.Storage; using Microsoft.UI.Xaml.Media.Imaging; using System; -using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Linq; @@ -20,7 +22,6 @@ using System.Threading; using System.Threading.Tasks; using System.Timers; using System.Windows.Input; -using Windows.ApplicationModel.Contacts; using Windows.ApplicationModel.DataTransfer; using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue; using Timer = System.Timers.Timer; @@ -30,6 +31,7 @@ namespace Harmonia.WinUI.ViewModels; public partial class PlaylistViewModel : ViewModelBase { private readonly IAudioPlayer _audioPlayer; + private readonly IAudioImageCache _audioImageCache; private readonly IAudioBitmapImageCache _audioBitmapImageCache; private readonly IAudioFileScanner _audioFileScanner; private readonly IAudioEngine _audioEngine; @@ -126,6 +128,7 @@ public partial class PlaylistViewModel : ViewModelBase public PlaylistViewModel( IAudioPlayer audioPlayer, + IAudioImageCache audioImageCache, IAudioBitmapImageCache audioBitmapImageCache, IAudioFileScanner audioFileScanner, IAudioEngine audioEngine, @@ -136,16 +139,27 @@ public partial class PlaylistViewModel : ViewModelBase _audioPlayer.PlaylistChanged += OnPlaylistChanged; _audioPlayer.PlayingSongChanged += OnPlayingSongChanged; + _audioImageCache = audioImageCache; _audioBitmapImageCache = audioBitmapImageCache; _audioFileScanner = audioFileScanner; _audioEngine = audioEngine; _storageProvider = storageProvider; _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + FilteredPlaylistSongs.CollectionChanged += OnFilteredPlaylistSongsCollectionChanged; + // Testing Task.Run(() => PlayDemoSong(playlistRepository)); } + private void OnFilteredPlaylistSongsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (IsUserUpdating == false) + return; + + int x = 1; + } + private async Task PlayDemoSong(IPlaylistRepository playlistRepository) { if (playlistRepository.Get().Count == 0) @@ -238,13 +252,24 @@ public partial class PlaylistViewModel : ViewModelBase await _audioPlayer.LoadAsync(playlistSong, PlaybackMode.LoadAndPlay); } + public async Task GetSongPictureInfoAsync(int hashCode, PlaylistSong playlistSong) + { + _imageCancellationTokens.TryGetValue(hashCode, out CancellationTokenSource? cancellationTokenSource); + cancellationTokenSource?.Cancel(); + + cancellationTokenSource = new(); + _imageCancellationTokens.AddOrUpdate(hashCode, cancellationTokenSource, (_, _) => cancellationTokenSource); + + return await _audioImageCache.GetAsync(playlistSong.Song, cancellationTokenSource.Token); + } + public async Task GetBitmapImageAsync(int hashCode, PlaylistSong playlistSong) { _imageCancellationTokens.TryGetValue(hashCode, out CancellationTokenSource? cancellationTokenSource); cancellationTokenSource?.Cancel(); cancellationTokenSource = new(); - _imageCancellationTokens.AddOrUpdate(hashCode, cancellationTokenSource, (_,_) => cancellationTokenSource); + _imageCancellationTokens.AddOrUpdate(hashCode, cancellationTokenSource, (_, _) => cancellationTokenSource); return await _audioBitmapImageCache.GetAsync(playlistSong.Song, cancellationTokenSource.Token); } @@ -290,7 +315,7 @@ public partial class PlaylistViewModel : ViewModelBase List filteredPlaylistSongs = [.. Playlist.Songs.Where(playlistSong => IsFiltered(playlistSong.Song))]; //FilteredPlaylistSongs = [.. filteredPlaylistSongs]; - for (int i = FilteredPlaylistSongs.Count -1; i >= 0; i--) + for (int i = FilteredPlaylistSongs.Count - 1; i >= 0; i--) { PlaylistSong playlistSong = FilteredPlaylistSongs[i]; @@ -382,7 +407,7 @@ public partial class PlaylistViewModel : ViewModelBase private FilePickerFileType GetAudioFileTypes() { - string[] patterns = _audioEngine.SupportedFormats.Select(format => format.Replace("*", "")).ToArray(); + string[] patterns = [.. _audioEngine.SupportedFormats.Select(format => format.Replace("*", ""))]; return new() { @@ -396,7 +421,7 @@ public partial class PlaylistViewModel : ViewModelBase if (Playlist == null) return; - string? path = await _storageProvider.GetPathAsync(); + string? path = await _storageProvider.GetPathAsync(); if (string.IsNullOrWhiteSpace(path)) return; @@ -448,7 +473,7 @@ public partial class PlaylistViewModel : ViewModelBase { if (SelectedPlaylistSongs.Count == 0) return; - + CopySelectedSongsToClipboard(); } diff --git a/Harmonia.WinUI/Views/PlaylistView.xaml b/Harmonia.WinUI/Views/PlaylistView.xaml index 1a6f05e..627e1a5 100644 --- a/Harmonia.WinUI/Views/PlaylistView.xaml +++ b/Harmonia.WinUI/Views/PlaylistView.xaml @@ -184,6 +184,11 @@ Name="PlaylistListView" ItemsSource="{Binding FilteredPlaylistSongs}" ItemTemplate="{StaticResource SongTemplate}" + CanReorderItems="True" + CanDragItems="True" + DragItemsStarting="PlaylistListView_DragItemsStarting" + DragItemsCompleted="PlaylistListView_DragItemsCompleted" + AllowDrop="True" SelectionMode="Extended" SelectionChanged="PlaylistListView_SelectionChanged"> diff --git a/Harmonia.WinUI/Views/PlaylistView.xaml.cs b/Harmonia.WinUI/Views/PlaylistView.xaml.cs index b318322..3b313e7 100644 --- a/Harmonia.WinUI/Views/PlaylistView.xaml.cs +++ b/Harmonia.WinUI/Views/PlaylistView.xaml.cs @@ -1,4 +1,5 @@ using CommunityToolkit.WinUI; +using Harmonia.Core.Imaging; using Harmonia.Core.Playlists; using Harmonia.WinUI.ViewModels; using Microsoft.UI.Dispatching; @@ -12,6 +13,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Threading.Tasks; using Windows.UI.Popups; namespace Harmonia.WinUI.Views; @@ -179,6 +181,7 @@ public sealed partial class PlaylistView : UserControl { int hashCode = image.GetHashCode(); //BitmapImage? bitmapImage = await _viewModel.GetBitmapImageAsync(hashCode, playlistSong); + //SongPictureInfo? songPictureInfo = await _viewModel.GetSongPictureInfoAsync(hashCode, playlistSong); DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () => { @@ -248,4 +251,14 @@ public sealed partial class PlaylistView : UserControl } } } + + private void PlaylistListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e) + { + _viewModel.IsUserUpdating = true; + } + + private void PlaylistListView_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args) + { + _viewModel.IsUserUpdating = false; + } } \ No newline at end of file