diff --git a/Harmonia.Core/Caching/Cache.cs b/Harmonia.Core/Caching/Cache.cs index c256fdd..5b36a31 100644 --- a/Harmonia.Core/Caching/Cache.cs +++ b/Harmonia.Core/Caching/Cache.cs @@ -32,30 +32,39 @@ public abstract class Cache : ICache where TKey : no SemaphoreSlim lockObject = _locks.GetOrAdd(actualKey, (key) => new SemaphoreSlim(1, 1)); + bool throttlerAcquired = false; + bool lockAcquired = false; + try { await _throttler.WaitAsync(cancellationToken); + throttlerAcquired = true; if (cancellationToken.IsCancellationRequested) return default; await lockObject.WaitAsync(cancellationToken); + lockAcquired = true; if (cancellationToken.IsCancellationRequested) return default; return TryGetValue(actualKey) ?? await FetchAsync2(key, cancellationToken); } + catch (OperationCanceledException) + { + return default; + } finally { - lockObject.Release(); + if (lockAcquired) + lockObject.Release(); _locks.TryRemove(lockObject, out _); - _throttler.Release(); + if (throttlerAcquired) + _throttler.Release(); } - - //return TryGetValue(actualKey) ?? Refresh(key); } private async Task FetchAsync2(TKey key, CancellationToken cancellationToken) diff --git a/Harmonia.WinUI/App.xaml.cs b/Harmonia.WinUI/App.xaml.cs index f7819d9..b2e55c9 100644 --- a/Harmonia.WinUI/App.xaml.cs +++ b/Harmonia.WinUI/App.xaml.cs @@ -22,7 +22,7 @@ public partial class App : Application //services.AddSingleton(); services.AddSingleton(); - //services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/Harmonia.WinUI/Harmonia.WinUI.csproj b/Harmonia.WinUI/Harmonia.WinUI.csproj index 8a79786..02c9142 100644 --- a/Harmonia.WinUI/Harmonia.WinUI.csproj +++ b/Harmonia.WinUI/Harmonia.WinUI.csproj @@ -65,6 +65,9 @@ Designer + + Designer + Designer diff --git a/Harmonia.WinUI/MainWindow.xaml b/Harmonia.WinUI/MainWindow.xaml index 835e214..16ead50 100644 --- a/Harmonia.WinUI/MainWindow.xaml +++ b/Harmonia.WinUI/MainWindow.xaml @@ -33,9 +33,9 @@ + - diff --git a/Harmonia.WinUI/Resources/Geometry.xaml b/Harmonia.WinUI/Resources/Geometry.xaml index 05f029c..b440101 100644 --- a/Harmonia.WinUI/Resources/Geometry.xaml +++ b/Harmonia.WinUI/Resources/Geometry.xaml @@ -126,6 +126,17 @@ M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4 + + M8 6.5a.5.5 0 0 1 .5.5v1.5H10a.5.5 0 0 1 0 1H8.5V11a.5.5 0 0 1-1 0V9.5H6a.5.5 0 0 1 0-1h1.5V7a.5.5 0 0 1 .5-.5 + M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5z + + + + m.5 3 .04.87a2 2 0 0 0-.342 1.311l.637 7A2 2 0 0 0 2.826 14H9v-1H2.826a1 1 0 0 1-.995-.91l-.637-7A1 1 0 0 1 2.19 4h11.62a1 1 0 0 1 .996 1.09L14.54 8h1.005l.256-2.819A2 2 0 0 0 13.81 3H9.828a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 6.172 1H2.5a2 2 0 0 0-2 2m5.672-1a1 1 0 0 1 .707.293L7.586 3H2.19q-.362.002-.683.12L1.5 2.98a1 1 0 0 1 1-.98z + M13.5 9a.5.5 0 0 1 .5.5V11h1.5a.5.5 0 1 1 0 1H14v1.5a.5.5 0 1 1-1 0V12h-1.5a.5.5 0 0 1 0-1H13V9.5a.5.5 0 0 1 .5-.5 + + + M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3m5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3m5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3 diff --git a/Harmonia.WinUI/Resources/Styles.xaml b/Harmonia.WinUI/Resources/Styles.xaml index bfd6895..8b976d2 100644 --- a/Harmonia.WinUI/Resources/Styles.xaml +++ b/Harmonia.WinUI/Resources/Styles.xaml @@ -70,5 +70,10 @@ - + + + + diff --git a/Harmonia.WinUI/ViewModels/PlayingSongViewModel.cs b/Harmonia.WinUI/ViewModels/PlayingSongViewModel.cs new file mode 100644 index 0000000..a5c8ee6 --- /dev/null +++ b/Harmonia.WinUI/ViewModels/PlayingSongViewModel.cs @@ -0,0 +1,79 @@ +using Harmonia.Core.Models; +using Harmonia.Core.Player; +using Harmonia.WinUI.Caching; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml.Media.Imaging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Harmonia.WinUI.ViewModels; + +public class PlayingSongViewModel : ViewModelBase +{ + private readonly IAudioPlayer _audioPlayer; + private readonly IAudioBitmapImageCache _audioBitmapImageCache; + private readonly DispatcherQueue _dispatcherQueue; + + private CancellationTokenSource? _songImageCancellationTokenSource; + + private Song? _song; + public Song? Song + { + get + { + return _song; + } + private set + { + SetProperty(ref _song, value); + } + } + + private BitmapImage? _songImageSource; + public BitmapImage? SongImageSource + { + get + { + return _songImageSource; + } + private set + { + SetProperty(ref _songImageSource, value); + } + } + + public PlayingSongViewModel(IAudioPlayer audioPlayer, IAudioBitmapImageCache audioBitmapImageCache) + { + _audioPlayer = audioPlayer; + _audioPlayer.PlayingSongChanged += OnAudioPlayerPlayingSongChanged; + + _audioBitmapImageCache = audioBitmapImageCache; + _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + } + + 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 (_songImageCancellationTokenSource != null) + await _songImageCancellationTokenSource.CancelAsync(); + + _songImageCancellationTokenSource = new(); + CancellationToken cancellationToken = _songImageCancellationTokenSource.Token; + + _dispatcherQueue.TryEnqueue(async () => + { + BitmapImage? bitmapImage = await _audioBitmapImageCache.GetAsync(Song, cancellationToken); + SongImageSource = bitmapImage; + }); + } +} \ No newline at end of file diff --git a/Harmonia.WinUI/ViewModels/PlaylistViewModel.cs b/Harmonia.WinUI/ViewModels/PlaylistViewModel.cs index 656b669..d78d6ff 100644 --- a/Harmonia.WinUI/ViewModels/PlaylistViewModel.cs +++ b/Harmonia.WinUI/ViewModels/PlaylistViewModel.cs @@ -6,9 +6,9 @@ using Harmonia.Core.Playlists; using Harmonia.Core.Scanner; using Harmonia.WinUI.Caching; using Harmonia.WinUI.Storage; -using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml.Media.Imaging; using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -20,8 +20,8 @@ using System.Threading; using System.Threading.Tasks; using System.Timers; using System.Windows.Input; +using Windows.ApplicationModel.Contacts; using Windows.ApplicationModel.DataTransfer; -using Windows.System; using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue; using Timer = System.Timers.Timer; @@ -288,7 +288,38 @@ public partial class PlaylistViewModel : ViewModelBase return; List filteredPlaylistSongs = [.. Playlist.Songs.Where(playlistSong => IsFiltered(playlistSong.Song))]; - FilteredPlaylistSongs = [.. filteredPlaylistSongs]; + //FilteredPlaylistSongs = [.. filteredPlaylistSongs]; + + for (int i = FilteredPlaylistSongs.Count -1; i >= 0; i--) + { + PlaylistSong playlistSong = FilteredPlaylistSongs[i]; + + bool inPlaylist = Playlist.Songs.Contains(playlistSong); + bool inFilter = filteredPlaylistSongs.Contains(playlistSong); + + if (!inPlaylist || !inFilter) + { + FilteredPlaylistSongs.Remove(playlistSong); + } + } + + int insertionIndex = 0; + + foreach (PlaylistSong playlistSong in Playlist.Songs) + { + bool inFilter = filteredPlaylistSongs.Contains(playlistSong); + bool inCurrentFilteredList = FilteredPlaylistSongs.Contains(playlistSong); + + if (inFilter) + { + if (!inCurrentFilteredList) + { + FilteredPlaylistSongs.Insert(insertionIndex, playlistSong); + } + + insertionIndex++; + } + } } private bool IsFiltered(Song song) diff --git a/Harmonia.WinUI/ViewModels/ViewModelLocator.cs b/Harmonia.WinUI/ViewModels/ViewModelLocator.cs index aa8cde5..6a1ac44 100644 --- a/Harmonia.WinUI/ViewModels/ViewModelLocator.cs +++ b/Harmonia.WinUI/ViewModels/ViewModelLocator.cs @@ -10,8 +10,8 @@ public class ViewModelLocator public static PlayerViewModel PlayerViewModel => App.ServiceProvider.GetRequiredService(); - //public static PlayingSongInfoViewModel PlayingSongInfoViewModel - // => App.ServiceProvider.GetRequiredService(); + public static PlayingSongViewModel PlayingSongViewModel + => App.ServiceProvider.GetRequiredService(); public static PlaylistViewModel PlaylistViewModel => App.ServiceProvider.GetRequiredService(); diff --git a/Harmonia.WinUI/Views/PlayingSongView.xaml b/Harmonia.WinUI/Views/PlayingSongView.xaml new file mode 100644 index 0000000..3680904 --- /dev/null +++ b/Harmonia.WinUI/Views/PlayingSongView.xaml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/Harmonia.WinUI/Views/PlayingSongView.xaml.cs b/Harmonia.WinUI/Views/PlayingSongView.xaml.cs new file mode 100644 index 0000000..ac6a884 --- /dev/null +++ b/Harmonia.WinUI/Views/PlayingSongView.xaml.cs @@ -0,0 +1,15 @@ +using Harmonia.WinUI.ViewModels; +using Microsoft.UI.Xaml.Controls; + +namespace Harmonia.WinUI.Views; + +public sealed partial class PlayingSongView : UserControl +{ + private readonly PlayingSongViewModel _viewModel; + + public PlayingSongView() + { + InitializeComponent(); + _viewModel = (PlayingSongViewModel)DataContext; + } +} \ No newline at end of file diff --git a/Harmonia.WinUI/Views/PlaylistView.xaml b/Harmonia.WinUI/Views/PlaylistView.xaml index 0d3e134..1a6f05e 100644 --- a/Harmonia.WinUI/Views/PlaylistView.xaml +++ b/Harmonia.WinUI/Views/PlaylistView.xaml @@ -30,10 +30,38 @@ + + + + + + + + + + - + @@ -55,15 +83,15 @@ - + - + - + - - - + + + @@ -88,16 +116,28 @@ - + + + + - + + + + - + + + + @@ -107,16 +147,32 @@ - - - - - + + + + + + - - - - + + + + + + + + + + + + + + + + @@ -147,9 +203,9 @@ - + - + diff --git a/Harmonia.WinUI/Views/PlaylistView.xaml.cs b/Harmonia.WinUI/Views/PlaylistView.xaml.cs index 8855c1b..b318322 100644 --- a/Harmonia.WinUI/Views/PlaylistView.xaml.cs +++ b/Harmonia.WinUI/Views/PlaylistView.xaml.cs @@ -6,9 +6,13 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; using System; +using System.Collections.Generic; +using System.ComponentModel; using System.Linq; +using Windows.UI.Popups; namespace Harmonia.WinUI.Views; @@ -21,6 +25,8 @@ public sealed partial class PlaylistView : UserControl InitializeComponent(); _viewModel = (PlaylistViewModel)DataContext; + _viewModel.PropertyChanging += OnViewModelPropertyChanging; + _viewModel.PropertyChanged += OnViewModelPropertyChanged; _viewModel.PlayingSongChangedAutomatically += OnPlayingSongChangedAutomatically; foreach (MenuFlyoutItemBase item in PlaylistListViewMenuFlyout.Items) @@ -29,6 +35,75 @@ public sealed partial class PlaylistView : UserControl } } + private void OnViewModelPropertyChanging(object? sender, PropertyChangingEventArgs e) + { + if (e.PropertyName == nameof(_viewModel.PlayingSong)) + { + ListViewItem listViewItem = (ListViewItem)PlaylistListView.ContainerFromItem(_viewModel.PlayingSong); + + if (listViewItem != null) + { + UpdateListViewItemStyle(listViewItem, false); + } + } + } + + private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(_viewModel.PlayingSong)) + { + ListViewItem listViewItem = (ListViewItem)PlaylistListView.ContainerFromItem(_viewModel.PlayingSong); + + UpdateListViewItemStyle(listViewItem, true); + } + } + + private void UpdateListViewItemStyle(FrameworkElement listViewItem, bool isPlaying) + { + if (listViewItem == null) + return; + + FrameworkElement? frameworkElement; + + if (listViewItem.Name == "PlaylistSongListViewItem") + { + frameworkElement = listViewItem; + } + else + { + if (listViewItem.FindDescendant("PlaylistSongListViewItem") is not FrameworkElement frameworkElement2) + return; + + frameworkElement = frameworkElement2; + } + + if (frameworkElement == null) + return; + + string songItemTitleBrushName = isPlaying ? "PlayingSongTitleTextBlock" : "SongTitleTextBlock"; + string songItemSubtitleBrushName = isPlaying ? "PlayingSongSubtitleTextBlock" : "SongSubtitleTextBlock"; + + UpdateElementStyle(frameworkElement, "SongTitleTextBlock", songItemTitleBrushName); + UpdateElementStyle(frameworkElement, "SongArtistsTextBlock", songItemSubtitleBrushName); + UpdateElementStyle(frameworkElement, "SongAlbumTextBlock", songItemSubtitleBrushName); + } + + private void UpdateElementStyle(FrameworkElement dependencyObject, string elementName, string resourceName) + { + if (dependencyObject.FindDescendant(elementName) is not FrameworkElement frameworkElement) + return; + + Resources.TryGetValue(resourceName, out object? resource); + + if (resource == null) + Application.Current.Resources.TryGetValue(resourceName, out resource); + + if (resource is not Style style) + return; + + frameworkElement.Style = style; + } + private void OnPlayingSongChangedAutomatically(object? sender, EventArgs e) { BringPlayingSongIntoView(); @@ -112,6 +187,16 @@ public sealed partial class PlaylistView : UserControl }); } + private void PlaylistSongListViewItem_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args) + { + if (args.NewValue is not PlaylistSong playlistSong) + return; + + bool isPlaying = playlistSong == _viewModel.PlayingSong; + + UpdateListViewItemStyle(sender, isPlaying); + } + private async void PlaylistListViewItem_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e) { if (sender is not FrameworkElement element)