Implemented tag refresh on a playlist. Made audio file scanning truly asynchronous. Added basic playlist sorting functionality.
This commit is contained in:
@@ -164,6 +164,8 @@ public class Playlist
|
||||
|
||||
public void ImportTags(Song[] songs)
|
||||
{
|
||||
List<PlaylistSong> 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()
|
||||
|
||||
@@ -17,5 +17,9 @@ public enum PlaylistUpdateAction
|
||||
/// <summary>
|
||||
/// The contents of the collection changed dramatically.
|
||||
/// </summary>
|
||||
Reset
|
||||
Reset,
|
||||
/// <summary>
|
||||
/// The tags of the songs in the collection were refreshed.
|
||||
/// </summary>
|
||||
Refresh
|
||||
}
|
||||
@@ -9,22 +9,19 @@ public class AudioFileScanner(IAudioEngine audioEngine, ITagResolver tagResolver
|
||||
{
|
||||
public async Task<Song[]> GetSongsAsync(string[] fileNames, CancellationToken cancellationToken)
|
||||
{
|
||||
List<Song> 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<Song> GetSongAsync(string fileName, CancellationToken cancellationToken)
|
||||
|
||||
@@ -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
|
||||
</x:String>
|
||||
|
||||
<x:String x:Key="SortIcon">
|
||||
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
|
||||
</x:String>
|
||||
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -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<SortItem> SortAllCommand { get; }
|
||||
public IRelayCommand<SortItem> 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<SortItem>(SortAllSongs);
|
||||
SortSelectedCommand = new RelayCommand<SortItem>(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<SortOption> SortOptions { get; set; } = [];
|
||||
}
|
||||
@@ -160,24 +160,30 @@
|
||||
</Style>
|
||||
</MenuFlyout.MenuFlyoutPresenterStyle>
|
||||
<MenuFlyoutItem Text="Refresh Tags" Command="{Binding RefreshTagsCommand}">
|
||||
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutItem Text="Remove Duplicates" Command="{Binding RemoveDuplicateSongsCommand}">
|
||||
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutItem Text="Remove Missing" Command="{Binding RemoveMissingSongsCommand}">
|
||||
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutItem Text="Lock Playlist">
|
||||
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
|
||||
<MenuFlyoutItem Text="Remove Playlist" Foreground="#ff99a4" Command="{Binding DeletePlaylistCommand}">
|
||||
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
<MenuFlyoutItem Text="Settings">
|
||||
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
|
||||
<MenuFlyoutSubItem x:Name="SortByMenuFlyoutSubItem" Text="Sort By...">
|
||||
</MenuFlyoutSubItem>
|
||||
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
|
||||
<MenuFlyoutItem Text="Settings">
|
||||
</MenuFlyoutItem>
|
||||
</MenuFlyout>
|
||||
</Button.Flyout>
|
||||
@@ -228,6 +234,12 @@
|
||||
</MenuFlyoutItem.KeyboardAccelerators>
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
<MenuFlyoutSubItem x:Name="SortSelectedByMenuFlyoutSubItem" Text="Sort By...">
|
||||
<MenuFlyoutSubItem.Icon>
|
||||
<PathIcon Data="{StaticResource SortIcon}"></PathIcon>
|
||||
</MenuFlyoutSubItem.Icon>
|
||||
</MenuFlyoutSubItem>
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
<MenuFlyoutItem Text="Cut" Command="{Binding CutSongsCommand}">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<PathIcon Data="{StaticResource CutIcon}"></PathIcon>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user