diff --git a/Harmonia.Core/Playlists/Playlist.cs b/Harmonia.Core/Playlists/Playlist.cs index 4e70cf2..c7f290a 100644 --- a/Harmonia.Core/Playlists/Playlist.cs +++ b/Harmonia.Core/Playlists/Playlist.cs @@ -14,6 +14,36 @@ public class Playlist public event EventHandler? PlaylistUpdated; + public void Lock() + { + IsLocked = true; + + PlaylistUpdatedEventArgs eventArgs = new() + { + Action = PlaylistUpdateAction.Lock, + Index = -1, + Count = 0, + Songs = [] + }; + + PlaylistUpdated?.Invoke(this, eventArgs); + } + + public void Unlock() + { + IsLocked = false; + + PlaylistUpdatedEventArgs eventArgs = new() + { + Action = PlaylistUpdateAction.Unlock, + Index = -1, + Count = 0, + Songs = [] + }; + + PlaylistUpdated?.Invoke(this, eventArgs); + } + public void AddSong(Song song, int? index = null) { AddSongs([song], index); @@ -83,6 +113,9 @@ public class Playlist public void SortSongs(PlaylistSong[] playlistSongs, SortOption[] sortOptions) { + if (IsLocked) + return; + Dictionary oldPlaylistSongs = playlistSongs .OrderBy(Songs.IndexOf) .ToDictionary(Songs.IndexOf, playlistSong => playlistSong); @@ -116,6 +149,50 @@ public class Playlist PlaylistUpdated?.Invoke(this, eventArgs); } + public void Randomize(PlaylistSong[] playlistSongs) + { + if (IsLocked) + return; + + int[] originalIndexes = [.. playlistSongs.Select(playlistSong => Songs.IndexOf(playlistSong))]; + PlaylistSong[] shuffledSongs = [.. playlistSongs.Shuffle()]; + + for (int i = 0; i < originalIndexes.Length; i++) + Songs[originalIndexes[i]] = shuffledSongs[i]; + + PlaylistUpdatedEventArgs eventArgs = new() + { + Action = PlaylistUpdateAction.Reset, + Index = -1, + Count = playlistSongs.Length, + Songs = playlistSongs + }; + + PlaylistUpdated?.Invoke(this, eventArgs); + } + + public void Reverse(PlaylistSong[] playlistSongs) + { + if (IsLocked) + return; + + int[] originalIndexes = [.. playlistSongs.Select(playlistSong => Songs.IndexOf(playlistSong))]; + PlaylistSong[] reversedSongs = [.. originalIndexes.Select(i => Songs[i]).Reverse()]; + + for (int i = 0; i < originalIndexes.Length; i++) + Songs[originalIndexes[i]] = reversedSongs[i]; + + PlaylistUpdatedEventArgs eventArgs = new() + { + Action = PlaylistUpdateAction.Reset, + Index = -1, + Count = playlistSongs.Length, + Songs = playlistSongs + }; + + PlaylistUpdated?.Invoke(this, eventArgs); + } + public void RemoveSong(int index) { RemoveSongs(index, 1); diff --git a/Harmonia.Core/Playlists/PlaylistUpdateAction.cs b/Harmonia.Core/Playlists/PlaylistUpdateAction.cs index 72abcba..6df5e11 100644 --- a/Harmonia.Core/Playlists/PlaylistUpdateAction.cs +++ b/Harmonia.Core/Playlists/PlaylistUpdateAction.cs @@ -21,5 +21,7 @@ public enum PlaylistUpdateAction /// /// The tags of the songs in the collection were refreshed. /// - Refresh + Refresh, + Lock, + Unlock } \ No newline at end of file diff --git a/Harmonia.WinUI/Resources/Styles.xaml b/Harmonia.WinUI/Resources/Styles.xaml index b4c6c70..f1254ea 100644 --- a/Harmonia.WinUI/Resources/Styles.xaml +++ b/Harmonia.WinUI/Resources/Styles.xaml @@ -16,6 +16,15 @@ + + + + + diff --git a/Harmonia.WinUI/ViewModels/PlaylistDetailViewModel.cs b/Harmonia.WinUI/ViewModels/PlaylistDetailViewModel.cs index 6618757..55b1a2c 100644 --- a/Harmonia.WinUI/ViewModels/PlaylistDetailViewModel.cs +++ b/Harmonia.WinUI/ViewModels/PlaylistDetailViewModel.cs @@ -147,6 +147,11 @@ public partial class PlaylistDetailViewModel : ViewModelBase public IAsyncRelayCommand DeletePlaylistCommand { get; } public IRelayCommand SortAllCommand { get; } public IRelayCommand SortSelectedCommand { get; } + public IRelayCommand RandomizeAllCommand { get; } + public IRelayCommand RandomizeSelectedCommand { get; } + public IRelayCommand ReverseAllCommand { get; } + public IRelayCommand ReverseSelectedCommand { get; } + public IRelayCommand ToggleLockCommand { get; } public bool IsUserUpdating { get; set; } private bool _isUserInitiatingSongChange; @@ -195,6 +200,11 @@ public partial class PlaylistDetailViewModel : ViewModelBase DeletePlaylistCommand = new AsyncRelayCommand(DeletePlaylistAsync); SortAllCommand = new RelayCommand(SortAllSongs); SortSelectedCommand = new RelayCommand(SortSelectedSongs, _ => AreMultipleSongsSelected()); + RandomizeAllCommand = new RelayCommand(RandomizeAllSongs); + RandomizeSelectedCommand = new RelayCommand(RandomizeSelectedSongs, AreMultipleSongsSelected); + ReverseAllCommand = new RelayCommand(ReverseAllSongs); + ReverseSelectedCommand = new RelayCommand(ReverseSelectedSongs, AreMultipleSongsSelected); + ToggleLockCommand = new RelayCommand(ToggleLock); FilteredPlaylistSongs.CollectionChanged += OnFilteredPlaylistSongsCollectionChanged; @@ -211,6 +221,8 @@ public partial class PlaylistDetailViewModel : ViewModelBase PasteSongsCommand.NotifyCanExecuteChanged(); OpenFileLocationCommand.NotifyCanExecuteChanged(); SortSelectedCommand.NotifyCanExecuteChanged(); + RandomizeSelectedCommand.NotifyCanExecuteChanged(); + ReverseSelectedCommand.NotifyCanExecuteChanged(); } private void OnFilteredPlaylistSongsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) @@ -669,7 +681,75 @@ public partial class PlaylistDetailViewModel : ViewModelBase if (SelectedPlaylistSongs.Count < 2) return; + string[] selectedPlaylistSongIds = [.. SelectedPlaylistSongs.Select(x => x.UID)]; + Playlist.SortSongs([.. SelectedPlaylistSongs], [.. sortItem.SortOptions]); + + RestoreSelectedSongs(selectedPlaylistSongIds); + } + + private void RestoreSelectedSongs(string[] playlistSongIds) + { + _dispatcherQueue.TryEnqueue(() => + { + PlaylistSong[] selectedPlaylistSongs = [.. Playlist?.Songs.Where(x => playlistSongIds.Contains(x.UID)) ?? []]; + SelectedPlaylistSongs = new ObservableCollection(selectedPlaylistSongs); + }); + } + + private void RandomizeAllSongs() + { + if (Playlist == null || Playlist.Songs.Count < 2) + return; + + Playlist.Randomize([.. Playlist.Songs]); + } + + private void RandomizeSelectedSongs() + { + if (Playlist == null || SelectedPlaylistSongs.Count < 2) + return; + + string[] selectedPlaylistSongIds = [.. SelectedPlaylistSongs.Select(x => x.UID)]; + + Playlist.Randomize([.. SelectedPlaylistSongs]); + + RestoreSelectedSongs(selectedPlaylistSongIds); + } + + private void ReverseAllSongs() + { + if (Playlist == null || Playlist.Songs.Count < 2) + return; + + Playlist.Reverse([.. Playlist.Songs]); + } + + private void ReverseSelectedSongs() + { + if (Playlist == null || SelectedPlaylistSongs.Count < 2) + return; + + string[] selectedPlaylistSongIds = [.. SelectedPlaylistSongs.Select(x => x.UID)]; + + Playlist.Reverse([.. SelectedPlaylistSongs]); + + RestoreSelectedSongs(selectedPlaylistSongIds); + } + + private void ToggleLock() + { + if (Playlist == null) + return; + + if (Playlist.IsLocked) + { + Playlist.Unlock(); + } + else + { + Playlist.Lock(); + } } #endregion diff --git a/Harmonia.WinUI/Views/PlaylistDetailView.xaml b/Harmonia.WinUI/Views/PlaylistDetailView.xaml index 8025b17..c035570 100644 --- a/Harmonia.WinUI/Views/PlaylistDetailView.xaml +++ b/Harmonia.WinUI/Views/PlaylistDetailView.xaml @@ -167,9 +167,9 @@ - - - + + + @@ -179,6 +179,11 @@ + + + + + @@ -238,6 +243,11 @@ + + + + + diff --git a/Harmonia.WinUI/Views/PlaylistDetailView.xaml.cs b/Harmonia.WinUI/Views/PlaylistDetailView.xaml.cs index 5751463..7907cf9 100644 --- a/Harmonia.WinUI/Views/PlaylistDetailView.xaml.cs +++ b/Harmonia.WinUI/Views/PlaylistDetailView.xaml.cs @@ -1,5 +1,4 @@ using CommunityToolkit.WinUI; -using Harmonia.Core.Imaging; using Harmonia.Core.Playlists; using Harmonia.WinUI.ViewModels; using Microsoft.UI.Dispatching; @@ -7,14 +6,10 @@ 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 System.Threading.Tasks; -using Windows.UI.Popups; namespace Harmonia.WinUI.Views; @@ -33,24 +28,28 @@ public sealed partial class PlaylistDetailView : UserControl foreach (MenuFlyoutItemBase item in PlaylistListViewMenuFlyout.Items) { - item.DataContextChanged += Item_DataContextChanged; + HookDataContextChanged(item); } + int currentSortItemIndex = 0; + foreach (SortItem sortItem in _viewModel.SortItems) { - SortByMenuFlyoutSubItem.Items.Add(new MenuFlyoutItem + SortByMenuFlyoutSubItem.Items.Insert(currentSortItemIndex, new MenuFlyoutItem { Text = sortItem.Name, Command = _viewModel.SortAllCommand, CommandParameter = sortItem }); - SortSelectedByMenuFlyoutSubItem.Items.Add(new MenuFlyoutItem + SortSelectedByMenuFlyoutSubItem.Items.Insert(currentSortItemIndex, new MenuFlyoutItem { Text = sortItem.Name, Command = _viewModel.SortSelectedCommand, CommandParameter = sortItem }); + + currentSortItemIndex++; } _viewModel.SortSelectedCommand.CanExecuteChanged += (_, _) => @@ -74,15 +73,44 @@ public sealed partial class PlaylistDetailView : UserControl private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) { - if (e.PropertyName == nameof(_viewModel.PlayingSong)) + switch (e.PropertyName) { - ListViewItem listViewItem = (ListViewItem)PlaylistListView.ContainerFromItem(_viewModel.PlayingSong); - - UpdateListViewItemStyle(listViewItem, true); + case nameof(_viewModel.PlayingSong): + ListViewItem listViewItem = (ListViewItem)PlaylistListView.ContainerFromItem(_viewModel.PlayingSong); + UpdateListViewItemStyle(listViewItem, true); + break; + case nameof(_viewModel.Playlist): + ScrollToTop(); + break; + case nameof(_viewModel.SelectedPlaylistSongs): + SyncListViewSelection(); + break; } - else if (e.PropertyName == nameof(_viewModel.Playlist)) + } + + private bool _isSyncingSelection; + + private void SyncListViewSelection() + { + _isSyncingSelection = true; + + try { - ScrollToTop(); + PlaylistListView.DeselectAll(); + + foreach (PlaylistSong playlistSong in _viewModel.SelectedPlaylistSongs) + { + int index = PlaylistListView.Items.IndexOf(playlistSong); + + if (index >= 0) + { + PlaylistListView.SelectRange(new ItemIndexRange(index, 1)); + } + } + } + finally + { + _isSyncingSelection = false; } } @@ -170,6 +198,19 @@ public sealed partial class PlaylistDetailView : UserControl } } + private void HookDataContextChanged(MenuFlyoutItemBase item) + { + item.DataContextChanged += Item_DataContextChanged; + + if (item is MenuFlyoutSubItem subItem) + { + foreach (MenuFlyoutItemBase childItem in subItem.Items) + { + HookDataContextChanged(childItem); + } + } + } + private void Item_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args) { if (sender is not MenuFlyoutItemBase item) @@ -268,6 +309,9 @@ public sealed partial class PlaylistDetailView : UserControl private void PlaylistListView_SelectionChanged(object sender, SelectionChangedEventArgs e) { + if (_isSyncingSelection) + return; + if (sender is not ListView listView) return;