using CommunityToolkit.Mvvm.Input; using Harmonia.Core.Engine; using Harmonia.Core.Models; using Harmonia.Core.Player; using Harmonia.Core.Playlists; using Harmonia.Core.Scanner; using Harmonia.WinUI.Caching; using Harmonia.WinUI.Storage; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml.Media.Imaging; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Timers; using System.Windows.Input; using Windows.ApplicationModel.DataTransfer; using Timer = System.Timers.Timer; namespace Harmonia.WinUI.ViewModels; public partial class PlaylistViewModel : ViewModelBase { private readonly IAudioPlayer _audioPlayer; private readonly IAudioBitmapImageCache _audioBitmapImageCache; private readonly IAudioFileScanner _audioFileScanner; private readonly IAudioEngine _audioEngine; private readonly IStorageProvider _storageProvider; private Timer? _filterTimer; public Playlist? Playlist { get; private set; } private PlaylistSong? _playingSong; public PlaylistSong? PlayingSong { get { return _playingSong; } set { SetProperty(ref _playingSong, value); } } private ObservableCollection _playlistSongs = []; public ObservableCollection PlaylistSongs { get { return _playlistSongs; } set { SetProperty(ref _playlistSongs, value); } } private string? _filter; public string? Filter { get { return _filter; } set { SetProperty(ref _filter, value); RestartFilterTimer(); } } private ObservableCollection _filteredPlaylistSongs = []; public ObservableCollection FilteredPlaylistSongs { get { return _filteredPlaylistSongs; } set { SetProperty(ref _filteredPlaylistSongs, value); } } private ObservableCollection _selectedPlaylistSongs = []; public ObservableCollection SelectedPlaylistSongs { get { return _selectedPlaylistSongs; } set { SetProperty(ref _selectedPlaylistSongs, value); } } public ICommand PlaySongCommand => new AsyncRelayCommand(PlaySongAsync, AreSongsSelected); 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 bool IsUserUpdating { get; set; } public PlaylistViewModel( IAudioPlayer audioPlayer, IAudioBitmapImageCache audioBitmapImageCache, IAudioFileScanner audioFileScanner, IAudioEngine audioEngine, IStorageProvider storageProvider) { _audioPlayer = audioPlayer; _audioPlayer.PlaylistChanged += OnPlaylistChanged; _audioPlayer.PlayingSongChanged += OnPlayingSongChanged; _audioBitmapImageCache = audioBitmapImageCache; _audioFileScanner = audioFileScanner; _audioEngine = audioEngine; _storageProvider = storageProvider; } private void OnPlaylistChanged(object? sender, EventArgs e) { if (Playlist != null) { Playlist.PlaylistUpdated -= OnPlaylistUpdated; } Playlist = _audioPlayer.Playlist; if (Playlist != null) { Playlist.PlaylistUpdated += OnPlaylistUpdated; } PlaylistSong[] playlistSongs = _audioPlayer.Playlist?.Songs.ToArray() ?? []; PlaylistSongs = [.. playlistSongs]; UpdateFilteredSongs(); } private void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e) { if (IsUserUpdating) return; switch (e.Action) { case PlaylistUpdateAction.Add: DispatcherQueue.GetForCurrentThread().TryEnqueue(() => AddSongs(e.Songs, e.Index)); break; case PlaylistUpdateAction.Remove: DispatcherQueue.GetForCurrentThread().TryEnqueue(() => RemoveSongsFromCollection(e.Songs)); break; } } private void AddSongs(PlaylistSong[] playlistSongs, int index = 0) { // TODO: Performance improvements int currentIndex = index; foreach (PlaylistSong playlistSong in playlistSongs) { PlaylistSongs.Insert(currentIndex++, playlistSong); } UpdateFilteredSongs(); } private void RemoveSongsFromCollection(PlaylistSong[] playlistSongs) { foreach (PlaylistSong playlistSong in playlistSongs) { PlaylistSongs.Remove(playlistSong); } UpdateFilteredSongs(); } private void OnPlayingSongChanged(object? sender, EventArgs e) { PlayingSong = _audioPlayer.PlayingSong; } public async Task PlaySongAsync(PlaylistSong playlistSong) { await _audioPlayer.LoadAsync(playlistSong, PlaybackMode.LoadAndPlay); } public async Task GetBitmapAsync(PlaylistSong playlistSong, CancellationToken cancellationToken) { return await _audioBitmapImageCache.GetAsync(playlistSong.Song, cancellationToken); } #region Filtering private void RestartFilterTimer() { if (_filterTimer == null) { _filterTimer = new Timer(300); _filterTimer.Elapsed += OnFilterTimerElapsed; _filterTimer.Start(); } else { _filterTimer.Interval = 300; } } private void OnFilterTimerElapsed(object? sender, ElapsedEventArgs e) { if (_filterTimer == null) return; _filterTimer.Stop(); _filterTimer.Dispose(); _filterTimer = null; DispatcherQueue.GetForCurrentThread().TryEnqueue(UpdateFilteredSongs); } private void UpdateFilteredSongs() { if (Playlist == null) return; List filteredPlaylistSongs = [.. Playlist.Songs.Where(playlistSong => IsFiltered(playlistSong.Song))]; FilteredPlaylistSongs = [.. filteredPlaylistSongs]; } private bool IsFiltered(Song song) { if (string.IsNullOrWhiteSpace(Filter)) return true; var shortFileName = Path.GetFileName(song.FileName); if (shortFileName.Contains(Filter, StringComparison.OrdinalIgnoreCase)) return true; if (string.IsNullOrWhiteSpace(song.Title) == false && song.Title.Contains(Filter, StringComparison.OrdinalIgnoreCase)) return true; if (string.IsNullOrWhiteSpace(song.Album) == false && song.Album.Contains(Filter, StringComparison.OrdinalIgnoreCase)) return true; if (song.AlbumArtists.Any(x => x.Contains(Filter, StringComparison.OrdinalIgnoreCase))) return true; if (song.Artists.Any(x => x.Contains(Filter, StringComparison.OrdinalIgnoreCase))) return true; return false; } #endregion #region Commands private async Task PlaySongAsync() { if (SelectedPlaylistSongs.Count == 0) return; await _audioPlayer.LoadAsync(SelectedPlaylistSongs[0], PlaybackMode.LoadAndPlay); } private bool AreSongsSelected() { return SelectedPlaylistSongs.Count > 0; } private async Task AddFilesAsync(CancellationToken cancellationToken) { if (Playlist == null) return; FilePickerOptions filePickerOptions = new() { FileTypeFilter = [GetAudioFileTypes()], }; string[] fileNames = await _storageProvider.GetFilesAsync(filePickerOptions); Song[] songs = await _audioFileScanner.GetSongsAsync(fileNames, cancellationToken); Playlist.AddSongs(songs); } private FilePickerFileType GetAudioFileTypes() { return new() { Name = "Audio Files", Patterns = [.. _audioEngine.SupportedFormats] }; } private async Task AddFolderAsync(CancellationToken cancellationToken) { if (Playlist == null) return; string path = await _storageProvider.GetPathAsync(); if (string.IsNullOrWhiteSpace(path)) return; Song[] songs = await _audioFileScanner.GetSongsFromPathAsync(path, cancellationToken); Playlist.AddSongs(songs); } public async Task AddFilesAsync(string[] fileNames, CancellationToken cancellationToken) { if (Playlist == null) return; Song[] songs = await _audioFileScanner.GetSongsAsync(fileNames, cancellationToken); Playlist.AddSongs(songs); } public async Task AddFolderAsync(string path, CancellationToken cancellationToken) { if (Playlist == null) return; Song[] songs = await _audioFileScanner.GetSongsFromPathAsync(path, cancellationToken); Playlist.AddSongs(songs); } private void RemoveSongs() { if (Playlist == null) return; if (SelectedPlaylistSongs.Count == 0) return; PlaylistSong[] playlistSongs = [.. SelectedPlaylistSongs]; Playlist.RemoveSongs(playlistSongs); } private void CutSongs() { if (SelectedPlaylistSongs.Count == 0) return; CopySelectedSongsToClipboard(); } private void CopySongs() { if (SelectedPlaylistSongs.Count == 0) return; CopySelectedSongsToClipboard(); } private void CopySelectedSongsToClipboard() { Song[] songs = [.. SelectedPlaylistSongs.Select(playlistSong => playlistSong.Song)]; DataPackage dataPackage = new() { RequestedOperation = DataPackageOperation.Copy }; dataPackage.Properties.Add("Type", "SongList"); dataPackage.SetData(StandardDataFormats.Text, JsonSerializer.Serialize(songs)); Clipboard.SetContent(dataPackage); } private bool CanPasteSongs() { DataPackageView dataPackageView = Clipboard.GetContent(); if (dataPackageView == null) return false; if (dataPackageView.Properties.ContainsKey("Type") == false) return false; return dataPackageView.Properties["Type"].ToString() == "SongList"; } private async Task PasteSongsAsync() { if (Playlist == null || SelectedPlaylistSongs.Count == 0) return; int selectedPlaylistSongIndex = Playlist.Songs.IndexOf(SelectedPlaylistSongs[0]); if (selectedPlaylistSongIndex == -1) return; Song[] songs = await GetSongsFromClipboardAsync(); Playlist.AddSongs(songs, selectedPlaylistSongIndex + 1); } private static async Task GetSongsFromClipboardAsync() { DataPackageView dataPackageView = Clipboard.GetContent(); string data = await dataPackageView.GetTextAsync(StandardDataFormats.Text); return JsonSerializer.Deserialize(data) ?? []; } private void OpenFileLocation() { if (SelectedPlaylistSongs.Count == 0) return; string argument = "/select, \"" + SelectedPlaylistSongs[0].Song.FileName + "\""; Process.Start("explorer.exe", argument); } private void RefreshTags() { //Playlist?.RefreshTags(); } private void RemoveMissingSongs() { Playlist?.RemoveMissingSongs(); } private void RemoveDuplicateSongs() { Playlist?.RemoveDuplicateSongs(); } #endregion }