Added randomize/reverse songs logic. Added initial UI-side locking logic.

This commit is contained in:
2026-08-03 22:38:16 -04:00
parent 99978c62c4
commit 2b78ac5667
6 changed files with 240 additions and 18 deletions

View File

@@ -14,6 +14,36 @@ public class Playlist
public event EventHandler<PlaylistUpdatedEventArgs>? PlaylistUpdated; 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) public void AddSong(Song song, int? index = null)
{ {
AddSongs([song], index); AddSongs([song], index);
@@ -83,6 +113,9 @@ public class Playlist
public void SortSongs(PlaylistSong[] playlistSongs, SortOption[] sortOptions) public void SortSongs(PlaylistSong[] playlistSongs, SortOption[] sortOptions)
{ {
if (IsLocked)
return;
Dictionary<int, PlaylistSong> oldPlaylistSongs = playlistSongs Dictionary<int, PlaylistSong> oldPlaylistSongs = playlistSongs
.OrderBy(Songs.IndexOf) .OrderBy(Songs.IndexOf)
.ToDictionary(Songs.IndexOf, playlistSong => playlistSong); .ToDictionary(Songs.IndexOf, playlistSong => playlistSong);
@@ -116,6 +149,50 @@ public class Playlist
PlaylistUpdated?.Invoke(this, eventArgs); 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) public void RemoveSong(int index)
{ {
RemoveSongs(index, 1); RemoveSongs(index, 1);

View File

@@ -21,5 +21,7 @@ public enum PlaylistUpdateAction
/// <summary> /// <summary>
/// The tags of the songs in the collection were refreshed. /// The tags of the songs in the collection were refreshed.
/// </summary> /// </summary>
Refresh Refresh,
Lock,
Unlock
} }

View File

@@ -16,6 +16,15 @@
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" /> <Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
</Style> </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}"> <Style TargetType="ListViewItem" BasedOn="{StaticResource DefaultListViewItemStyle}">
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" /> <Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
</Style> </Style>

View File

@@ -147,6 +147,11 @@ public partial class PlaylistDetailViewModel : ViewModelBase
public IAsyncRelayCommand DeletePlaylistCommand { get; } public IAsyncRelayCommand DeletePlaylistCommand { get; }
public IRelayCommand<SortItem> SortAllCommand { get; } public IRelayCommand<SortItem> SortAllCommand { get; }
public IRelayCommand<SortItem> SortSelectedCommand { 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; } public bool IsUserUpdating { get; set; }
private bool _isUserInitiatingSongChange; private bool _isUserInitiatingSongChange;
@@ -195,6 +200,11 @@ public partial class PlaylistDetailViewModel : ViewModelBase
DeletePlaylistCommand = new AsyncRelayCommand(DeletePlaylistAsync); DeletePlaylistCommand = new AsyncRelayCommand(DeletePlaylistAsync);
SortAllCommand = new RelayCommand<SortItem>(SortAllSongs); SortAllCommand = new RelayCommand<SortItem>(SortAllSongs);
SortSelectedCommand = new RelayCommand<SortItem>(SortSelectedSongs, _ => AreMultipleSongsSelected()); 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; FilteredPlaylistSongs.CollectionChanged += OnFilteredPlaylistSongsCollectionChanged;
@@ -211,6 +221,8 @@ public partial class PlaylistDetailViewModel : ViewModelBase
PasteSongsCommand.NotifyCanExecuteChanged(); PasteSongsCommand.NotifyCanExecuteChanged();
OpenFileLocationCommand.NotifyCanExecuteChanged(); OpenFileLocationCommand.NotifyCanExecuteChanged();
SortSelectedCommand.NotifyCanExecuteChanged(); SortSelectedCommand.NotifyCanExecuteChanged();
RandomizeSelectedCommand.NotifyCanExecuteChanged();
ReverseSelectedCommand.NotifyCanExecuteChanged();
} }
private void OnFilteredPlaylistSongsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) private void OnFilteredPlaylistSongsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
@@ -669,7 +681,75 @@ public partial class PlaylistDetailViewModel : ViewModelBase
if (SelectedPlaylistSongs.Count < 2) if (SelectedPlaylistSongs.Count < 2)
return; return;
string[] selectedPlaylistSongIds = [.. SelectedPlaylistSongs.Select(x => x.UID)];
Playlist.SortSongs([.. SelectedPlaylistSongs], [.. sortItem.SortOptions]); 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 #endregion

View File

@@ -167,9 +167,9 @@
<MenuFlyoutItem Text="Remove Missing" Command="{Binding RemoveMissingSongsCommand}"> <MenuFlyoutItem Text="Remove Missing" Command="{Binding RemoveMissingSongsCommand}">
</MenuFlyoutItem> </MenuFlyoutItem>
<MenuFlyoutItem Text="Lock Playlist"> <ToggleMenuFlyoutItem Text="Lock Playlist" IsChecked="{Binding Playlist.IsLocked}">
</MenuFlyoutItem> </ToggleMenuFlyoutItem>
<MenuFlyoutSeparator></MenuFlyoutSeparator> <MenuFlyoutSeparator></MenuFlyoutSeparator>
@@ -179,6 +179,11 @@
<MenuFlyoutSeparator></MenuFlyoutSeparator> <MenuFlyoutSeparator></MenuFlyoutSeparator>
<MenuFlyoutSubItem x:Name="SortByMenuFlyoutSubItem" Text="Sort By..."> <MenuFlyoutSubItem x:Name="SortByMenuFlyoutSubItem" Text="Sort By...">
<MenuFlyoutSeparator></MenuFlyoutSeparator>
<MenuFlyoutItem Text="Randomize" Command="{Binding RandomizeAllCommand}">
</MenuFlyoutItem>
<MenuFlyoutItem Text="Reverse" Command="{Binding ReverseAllCommand}">
</MenuFlyoutItem>
</MenuFlyoutSubItem> </MenuFlyoutSubItem>
<MenuFlyoutSeparator></MenuFlyoutSeparator> <MenuFlyoutSeparator></MenuFlyoutSeparator>
@@ -238,6 +243,11 @@
<MenuFlyoutSubItem.Icon> <MenuFlyoutSubItem.Icon>
<PathIcon Data="{StaticResource SortIcon}"></PathIcon> <PathIcon Data="{StaticResource SortIcon}"></PathIcon>
</MenuFlyoutSubItem.Icon> </MenuFlyoutSubItem.Icon>
<MenuFlyoutSeparator></MenuFlyoutSeparator>
<MenuFlyoutItem Text="Randomize" Command="{Binding RandomizeSelectedCommand}">
</MenuFlyoutItem>
<MenuFlyoutItem Text="Reverse" Command="{Binding ReverseSelectedCommand}">
</MenuFlyoutItem>
</MenuFlyoutSubItem> </MenuFlyoutSubItem>
<MenuFlyoutSeparator></MenuFlyoutSeparator> <MenuFlyoutSeparator></MenuFlyoutSeparator>
<MenuFlyoutItem Text="Cut" Command="{Binding CutSongsCommand}"> <MenuFlyoutItem Text="Cut" Command="{Binding CutSongsCommand}">

View File

@@ -1,5 +1,4 @@
using CommunityToolkit.WinUI; using CommunityToolkit.WinUI;
using Harmonia.Core.Imaging;
using Harmonia.Core.Playlists; using Harmonia.Core.Playlists;
using Harmonia.WinUI.ViewModels; using Harmonia.WinUI.ViewModels;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
@@ -7,14 +6,10 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.UI.Xaml.Media.Imaging;
using System; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Windows.UI.Popups;
namespace Harmonia.WinUI.Views; namespace Harmonia.WinUI.Views;
@@ -33,24 +28,28 @@ public sealed partial class PlaylistDetailView : UserControl
foreach (MenuFlyoutItemBase item in PlaylistListViewMenuFlyout.Items) foreach (MenuFlyoutItemBase item in PlaylistListViewMenuFlyout.Items)
{ {
item.DataContextChanged += Item_DataContextChanged; HookDataContextChanged(item);
} }
int currentSortItemIndex = 0;
foreach (SortItem sortItem in _viewModel.SortItems) foreach (SortItem sortItem in _viewModel.SortItems)
{ {
SortByMenuFlyoutSubItem.Items.Add(new MenuFlyoutItem SortByMenuFlyoutSubItem.Items.Insert(currentSortItemIndex, new MenuFlyoutItem
{ {
Text = sortItem.Name, Text = sortItem.Name,
Command = _viewModel.SortAllCommand, Command = _viewModel.SortAllCommand,
CommandParameter = sortItem CommandParameter = sortItem
}); });
SortSelectedByMenuFlyoutSubItem.Items.Add(new MenuFlyoutItem SortSelectedByMenuFlyoutSubItem.Items.Insert(currentSortItemIndex, new MenuFlyoutItem
{ {
Text = sortItem.Name, Text = sortItem.Name,
Command = _viewModel.SortSelectedCommand, Command = _viewModel.SortSelectedCommand,
CommandParameter = sortItem CommandParameter = sortItem
}); });
currentSortItemIndex++;
} }
_viewModel.SortSelectedCommand.CanExecuteChanged += (_, _) => _viewModel.SortSelectedCommand.CanExecuteChanged += (_, _) =>
@@ -74,15 +73,44 @@ public sealed partial class PlaylistDetailView : UserControl
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{ {
if (e.PropertyName == nameof(_viewModel.PlayingSong)) switch (e.PropertyName)
{ {
ListViewItem listViewItem = (ListViewItem)PlaylistListView.ContainerFromItem(_viewModel.PlayingSong); case nameof(_viewModel.PlayingSong):
ListViewItem listViewItem = (ListViewItem)PlaylistListView.ContainerFromItem(_viewModel.PlayingSong);
UpdateListViewItemStyle(listViewItem, true); 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) private void Item_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{ {
if (sender is not MenuFlyoutItemBase item) if (sender is not MenuFlyoutItemBase item)
@@ -268,6 +309,9 @@ public sealed partial class PlaylistDetailView : UserControl
private void PlaylistListView_SelectionChanged(object sender, SelectionChangedEventArgs e) private void PlaylistListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{ {
if (_isSyncingSelection)
return;
if (sender is not ListView listView) if (sender is not ListView listView)
return; return;