diff --git a/Harmonia.Core/Playlists/Playlist.cs b/Harmonia.Core/Playlists/Playlist.cs index dc027b7..4e70cf2 100644 --- a/Harmonia.Core/Playlists/Playlist.cs +++ b/Harmonia.Core/Playlists/Playlist.cs @@ -164,6 +164,8 @@ public class Playlist public void ImportTags(Song[] songs) { + List updatedPlaylistSongs = []; + foreach (Song song in songs) { PlaylistSong[] playlistSongs = [.. Songs.Where(playlistSong => @@ -171,10 +173,24 @@ public class Playlist foreach (PlaylistSong playlistSong in playlistSongs) { - //playlistSong.Song = song; - //playlistSong.Song.Update(song); + playlistSong.Song.Update(song); } + + updatedPlaylistSongs.AddRange(playlistSongs); } + + if (updatedPlaylistSongs.Count == 0) + return; + + PlaylistUpdatedEventArgs eventArgs = new() + { + Action = PlaylistUpdateAction.Refresh, + Index = -1, + Count = updatedPlaylistSongs.Count, + Songs = [.. updatedPlaylistSongs] + }; + + PlaylistUpdated?.Invoke(this, eventArgs); } public void RemoveMissingSongs() diff --git a/Harmonia.Core/Playlists/PlaylistUpdateAction.cs b/Harmonia.Core/Playlists/PlaylistUpdateAction.cs index d192184..72abcba 100644 --- a/Harmonia.Core/Playlists/PlaylistUpdateAction.cs +++ b/Harmonia.Core/Playlists/PlaylistUpdateAction.cs @@ -17,5 +17,9 @@ public enum PlaylistUpdateAction /// /// The contents of the collection changed dramatically. /// - Reset + Reset, + /// + /// The tags of the songs in the collection were refreshed. + /// + Refresh } \ No newline at end of file diff --git a/Harmonia.Core/Scanner/AudioFileScanner.cs b/Harmonia.Core/Scanner/AudioFileScanner.cs index a4236cc..1371e2b 100644 --- a/Harmonia.Core/Scanner/AudioFileScanner.cs +++ b/Harmonia.Core/Scanner/AudioFileScanner.cs @@ -9,22 +9,19 @@ public class AudioFileScanner(IAudioEngine audioEngine, ITagResolver tagResolver { public async Task GetSongsAsync(string[] fileNames, CancellationToken cancellationToken) { - List songs = []; + Song?[] songs = new Song?[fileNames.Length]; - foreach (string fileName in fileNames) + await Parallel.ForEachAsync(Enumerable.Range(0, fileNames.Length), cancellationToken, async (index, token) => { + string fileName = fileNames[index]; + if (string.IsNullOrWhiteSpace(fileName) || File.Exists(fileName) == false) - continue; + return; - Song? song = await GetSongAsync(fileName, cancellationToken); + songs[index] = await GetSongAsync(fileName, token); + }); - if (song == null) - continue; - - songs.Add(song); - } - - return [.. songs]; + return [.. songs.Where(song => song != null)!]; } private async Task GetSongAsync(string fileName, CancellationToken cancellationToken) diff --git a/Harmonia.WinUI/Resources/Geometry.xaml b/Harmonia.WinUI/Resources/Geometry.xaml index bc8b164..4ffce95 100644 --- a/Harmonia.WinUI/Resources/Geometry.xaml +++ b/Harmonia.WinUI/Resources/Geometry.xaml @@ -148,4 +148,8 @@ M0 11.5a.5.5 0 0 1 .5-.5H4a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5m0-4A.5.5 0 0 1 .5 7H8a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5m0-4A.5.5 0 0 1 .5 3H8a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5 + + M7 5C7.55228 5 8 5.44772 8 6V15.5858L10.2929 13.2929C10.6834 12.9024 11.3166 12.9024 11.7071 13.2929C12.0976 13.6834 12.0976 14.3166 11.7071 14.7071L7.70711 18.7071C7.31658 19.0976 6.68342 19.0976 6.29289 18.7071L2.29289 14.7071C1.90237 14.3166 1.90237 13.6834 2.29289 13.2929C2.68342 12.9024 3.31658 12.9024 3.70711 13.2929L6 15.5858V6C6 5.44772 6.44772 5 7 5ZM16.2929 5.29289C16.6834 4.90237 17.3166 4.90237 17.7071 5.29289L21.7071 9.29289C22.0976 9.68342 22.0976 10.3166 21.7071 10.7071C21.3166 11.0976 20.6834 11.0976 20.2929 10.7071L18 8.41421V18C18 18.5523 17.5523 19 17 19C16.4477 19 16 18.5523 16 18V8.41421L13.7071 10.7071C13.3166 11.0976 12.6834 11.0976 12.2929 10.7071C11.9024 10.3166 11.9024 9.68342 12.2929 9.29289L16.2929 5.29289Z + + diff --git a/Harmonia.WinUI/ViewModels/PlaylistDetailViewModel.cs b/Harmonia.WinUI/ViewModels/PlaylistDetailViewModel.cs index 370ceb0..6618757 100644 --- a/Harmonia.WinUI/ViewModels/PlaylistDetailViewModel.cs +++ b/Harmonia.WinUI/ViewModels/PlaylistDetailViewModel.cs @@ -15,13 +15,13 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using System.Windows.Input; using Windows.ApplicationModel.DataTransfer; using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue; using DispatcherQueueTimer = Microsoft.UI.Dispatching.DispatcherQueueTimer; @@ -103,22 +103,50 @@ public partial class PlaylistDetailViewModel : ViewModelBase set { SetProperty(ref _selectedPlaylistSongs, value); + NotifySelectionDependentCommands(); } } - public ICommand PlaySongCommand => new AsyncRelayCommand(PlaySongAsync, AreSongsSelected); - public ICommand NewPlaylistCommand => new AsyncRelayCommand(NewPlaylistAsync); - public ICommand AddFilesCommand => new AsyncRelayCommand(AddFilesAsync); - public ICommand AddFolderCommand => new AsyncRelayCommand(AddFolderAsync); - public ICommand RemoveSongsCommand => new RelayCommand(RemoveSongs, AreSongsSelected); - public ICommand CutSongsCommand => new RelayCommand(CutSongs, AreSongsSelected); - public ICommand CopySongsCommand => new RelayCommand(CopySongs, AreSongsSelected); - public ICommand PasteSongsCommand => new AsyncRelayCommand(PasteSongsAsync, CanPasteSongs); - public ICommand OpenFileLocationCommand => new RelayCommand(OpenFileLocation, AreSongsSelected); - public ICommand RefreshTagsCommand => new RelayCommand(RefreshTags); - public ICommand RemoveMissingSongsCommand => new RelayCommand(RemoveMissingSongs); - public ICommand RemoveDuplicateSongsCommand => new RelayCommand(RemoveDuplicateSongs); - public ICommand DeletePlaylistCommand => new AsyncRelayCommand(DeletePlaylistAsync); + public SortItem[] SortItems { get; } = + [ + new SortItem + { + Name = "Album - Disc # - Track # - Title - File Name", + SortOptions = + [ + new(nameof(Song.Album), ListSortDirection.Ascending), + new(nameof(Song.DiscNumber), ListSortDirection.Ascending), + new(nameof(Song.TrackNumber), ListSortDirection.Ascending), + new(nameof(Song.Title), ListSortDirection.Ascending), + new(nameof(Song.FileName), ListSortDirection.Ascending) + ] + }, + new SortItem + { + Name = "Title - File Name", + SortOptions = + [ + new(nameof(Song.Title), ListSortDirection.Ascending), + new(nameof(Song.FileName), ListSortDirection.Ascending) + ] + } + ]; + + public IAsyncRelayCommand PlaySongCommand { get; } + public IAsyncRelayCommand NewPlaylistCommand { get; } + public IAsyncRelayCommand AddFilesCommand { get; } + public IAsyncRelayCommand AddFolderCommand { get; } + public IRelayCommand RemoveSongsCommand { get; } + public IRelayCommand CutSongsCommand { get; } + public IRelayCommand CopySongsCommand { get; } + public IAsyncRelayCommand PasteSongsCommand { get; } + public IRelayCommand OpenFileLocationCommand { get; } + public IAsyncRelayCommand RefreshTagsCommand { get; } + public IRelayCommand RemoveMissingSongsCommand { get; } + public IRelayCommand RemoveDuplicateSongsCommand { get; } + public IAsyncRelayCommand DeletePlaylistCommand { get; } + public IRelayCommand SortAllCommand { get; } + public IRelayCommand SortSelectedCommand { get; } public bool IsUserUpdating { get; set; } private bool _isUserInitiatingSongChange; @@ -152,12 +180,39 @@ public partial class PlaylistDetailViewModel : ViewModelBase _messageService = messageService; _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + PlaySongCommand = new AsyncRelayCommand(PlaySongAsync, AreSongsSelected); + NewPlaylistCommand = new AsyncRelayCommand(NewPlaylistAsync); + AddFilesCommand = new AsyncRelayCommand(AddFilesAsync); + AddFolderCommand = new AsyncRelayCommand(AddFolderAsync); + RemoveSongsCommand = new RelayCommand(RemoveSongs, AreSongsSelected); + CutSongsCommand = new RelayCommand(CutSongs, AreSongsSelected); + CopySongsCommand = new RelayCommand(CopySongs, AreSongsSelected); + PasteSongsCommand = new AsyncRelayCommand(PasteSongsAsync, CanPasteSongs); + OpenFileLocationCommand = new RelayCommand(OpenFileLocation, AreSongsSelected); + RefreshTagsCommand = new AsyncRelayCommand(RefreshTagsAsync); + RemoveMissingSongsCommand = new RelayCommand(RemoveMissingSongs); + RemoveDuplicateSongsCommand = new RelayCommand(RemoveDuplicateSongs); + DeletePlaylistCommand = new AsyncRelayCommand(DeletePlaylistAsync); + SortAllCommand = new RelayCommand(SortAllSongs); + SortSelectedCommand = new RelayCommand(SortSelectedSongs, _ => AreMultipleSongsSelected()); + FilteredPlaylistSongs.CollectionChanged += OnFilteredPlaylistSongsCollectionChanged; Playlist = _playlistManager.CurrentPlaylist; // Testing UpdateFilteredSongs(); } + private void NotifySelectionDependentCommands() + { + PlaySongCommand.NotifyCanExecuteChanged(); + RemoveSongsCommand.NotifyCanExecuteChanged(); + CutSongsCommand.NotifyCanExecuteChanged(); + CopySongsCommand.NotifyCanExecuteChanged(); + PasteSongsCommand.NotifyCanExecuteChanged(); + OpenFileLocationCommand.NotifyCanExecuteChanged(); + SortSelectedCommand.NotifyCanExecuteChanged(); + } + private void OnFilteredPlaylistSongsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (IsUserUpdating == false) @@ -212,6 +267,7 @@ public partial class PlaylistDetailViewModel : ViewModelBase { case PlaylistUpdateAction.Add: case PlaylistUpdateAction.Remove: + case PlaylistUpdateAction.Refresh: _dispatcherQueue.TryEnqueue(UpdateFilteredSongs); break; case PlaylistUpdateAction.Move: @@ -381,6 +437,11 @@ public partial class PlaylistDetailViewModel : ViewModelBase return SelectedPlaylistSongs.Count > 0; } + private bool AreMultipleSongsSelected() + { + return SelectedPlaylistSongs.Count > 1; + } + private async Task NewPlaylistAsync(CancellationToken cancellationToken) { Playlist playlist = await _playlistManager.AddPlaylistAsync(); @@ -538,9 +599,23 @@ public partial class PlaylistDetailViewModel : ViewModelBase Process.Start("explorer.exe", argument); } - private void RefreshTags() + private async Task RefreshTagsAsync(CancellationToken cancellationToken) { - //Playlist?.RefreshTags(); + if (Playlist is null) + return; + + // TODO: Have different method for selected songs versus all songs in the playlist + PlaylistSong[] targets = SelectedPlaylistSongs.Count > 0 + ? [.. SelectedPlaylistSongs] + : [.. Playlist.Songs]; + + string[] fileNames = [.. targets + .Select(ps => ps.Song.FileName) + .Distinct(StringComparer.OrdinalIgnoreCase)]; + + Song[] songs = await _audioFileScanner.GetSongsAsync(fileNames, cancellationToken); + + Playlist.ImportTags(songs); } private void RemoveMissingSongs() @@ -578,5 +653,30 @@ public partial class PlaylistDetailViewModel : ViewModelBase } } + private void SortAllSongs(SortItem? sortItem) + { + if (Playlist == null || sortItem == null) + return; + + Playlist.SortSongs([.. Playlist.Songs], [.. sortItem.SortOptions]); + } + + private void SortSelectedSongs(SortItem? sortItem) + { + if (Playlist == null || sortItem == null) + return; + + if (SelectedPlaylistSongs.Count < 2) + return; + + Playlist.SortSongs([.. SelectedPlaylistSongs], [.. sortItem.SortOptions]); + } + #endregion +} + +public class SortItem +{ + public string Name { get; set; } = string.Empty; + public IReadOnlyCollection SortOptions { get; set; } = []; } \ No newline at end of file diff --git a/Harmonia.WinUI/Views/PlaylistDetailView.xaml b/Harmonia.WinUI/Views/PlaylistDetailView.xaml index 22d2103..8025b17 100644 --- a/Harmonia.WinUI/Views/PlaylistDetailView.xaml +++ b/Harmonia.WinUI/Views/PlaylistDetailView.xaml @@ -160,24 +160,30 @@ - + - + - + - + + - + + + + + + + - @@ -228,6 +234,12 @@ + + + + + + diff --git a/Harmonia.WinUI/Views/PlaylistDetailView.xaml.cs b/Harmonia.WinUI/Views/PlaylistDetailView.xaml.cs index 7b024d0..5751463 100644 --- a/Harmonia.WinUI/Views/PlaylistDetailView.xaml.cs +++ b/Harmonia.WinUI/Views/PlaylistDetailView.xaml.cs @@ -35,6 +35,28 @@ public sealed partial class PlaylistDetailView : UserControl { item.DataContextChanged += Item_DataContextChanged; } + + foreach (SortItem sortItem in _viewModel.SortItems) + { + SortByMenuFlyoutSubItem.Items.Add(new MenuFlyoutItem + { + Text = sortItem.Name, + Command = _viewModel.SortAllCommand, + CommandParameter = sortItem + }); + + SortSelectedByMenuFlyoutSubItem.Items.Add(new MenuFlyoutItem + { + Text = sortItem.Name, + Command = _viewModel.SortSelectedCommand, + CommandParameter = sortItem + }); + } + + _viewModel.SortSelectedCommand.CanExecuteChanged += (_, _) => + SortSelectedByMenuFlyoutSubItem.IsEnabled = _viewModel.SortSelectedCommand.CanExecute(null); + + SortSelectedByMenuFlyoutSubItem.IsEnabled = _viewModel.SortSelectedCommand.CanExecute(null); } private void OnViewModelPropertyChanging(object? sender, PropertyChangingEventArgs e)