Added randomize/reverse songs logic. Added initial UI-side locking logic.
This commit is contained in:
@@ -14,6 +14,36 @@ public class Playlist
|
||||
|
||||
public event EventHandler<PlaylistUpdatedEventArgs>? 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<int, PlaylistSong> 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);
|
||||
|
||||
@@ -21,5 +21,7 @@ public enum PlaylistUpdateAction
|
||||
/// <summary>
|
||||
/// The tags of the songs in the collection were refreshed.
|
||||
/// </summary>
|
||||
Refresh
|
||||
Refresh,
|
||||
Lock,
|
||||
Unlock
|
||||
}
|
||||
@@ -16,6 +16,15 @@
|
||||
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="MenuFlyoutSubItem" BasedOn="{StaticResource DefaultMenuFlyoutSubItemStyle}">
|
||||
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ToggleMenuFlyoutItem" BasedOn="{StaticResource DefaultToggleMenuFlyoutItemStyle}">
|
||||
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
|
||||
</Style>
|
||||
|
||||
|
||||
<Style TargetType="ListViewItem" BasedOn="{StaticResource DefaultListViewItemStyle}">
|
||||
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
|
||||
</Style>
|
||||
|
||||
@@ -147,6 +147,11 @@ public partial class PlaylistDetailViewModel : ViewModelBase
|
||||
public IAsyncRelayCommand DeletePlaylistCommand { get; }
|
||||
public IRelayCommand<SortItem> SortAllCommand { get; }
|
||||
public IRelayCommand<SortItem> 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<SortItem>(SortAllSongs);
|
||||
SortSelectedCommand = new RelayCommand<SortItem>(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<PlaylistSong>(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
|
||||
|
||||
@@ -168,8 +168,8 @@
|
||||
<MenuFlyoutItem Text="Remove Missing" Command="{Binding RemoveMissingSongsCommand}">
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutItem Text="Lock Playlist">
|
||||
</MenuFlyoutItem>
|
||||
<ToggleMenuFlyoutItem Text="Lock Playlist" IsChecked="{Binding Playlist.IsLocked}">
|
||||
</ToggleMenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
|
||||
@@ -179,6 +179,11 @@
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
|
||||
<MenuFlyoutSubItem x:Name="SortByMenuFlyoutSubItem" Text="Sort By...">
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
<MenuFlyoutItem Text="Randomize" Command="{Binding RandomizeAllCommand}">
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutItem Text="Reverse" Command="{Binding ReverseAllCommand}">
|
||||
</MenuFlyoutItem>
|
||||
</MenuFlyoutSubItem>
|
||||
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
@@ -238,6 +243,11 @@
|
||||
<MenuFlyoutSubItem.Icon>
|
||||
<PathIcon Data="{StaticResource SortIcon}"></PathIcon>
|
||||
</MenuFlyoutSubItem.Icon>
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
<MenuFlyoutItem Text="Randomize" Command="{Binding RandomizeSelectedCommand}">
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutItem Text="Reverse" Command="{Binding ReverseSelectedCommand}">
|
||||
</MenuFlyoutItem>
|
||||
</MenuFlyoutSubItem>
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
<MenuFlyoutItem Text="Cut" Command="{Binding CutSongsCommand}">
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user