using Avalonia.Input.Platform; using Avalonia.Media.Imaging; using Avalonia.Platform.Storage; using Avalonia.Threading; 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.UI.Caching; using Harmonia.UI.Platform; using System; using System.Collections.Concurrent; 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 Timer = System.Timers.Timer; namespace Harmonia.UI.ViewModels; public class PlaylistViewModel : ViewModelBase { private readonly IAudioPlayer _audioPlayer; private readonly IAudioBitmapCache _audioBitmapImageCache; private readonly IAudioFileScanner _audioFileScanner; private readonly IAudioEngine _audioEngine; private readonly IStorageProviderLocator _storageProviderLocator; private readonly IClipboardLocator _clipboardLocator; private readonly ConcurrentDictionary _bitmapDictionary = []; 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 { _playlistSongs = value; OnPropertyChanged(); } } private string? _filter; public string? Filter { get { return _filter; } set { _filter = value; OnPropertyChanged(); 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 { _selectedPlaylistSongs = value; OnPropertyChanged(); } } 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 AsyncRelayCommand(CutSongsAsync, AreSongsSelected); public ICommand CopySongsCommand => new AsyncRelayCommand(CopySongsAsync, 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 PlaylistViewModel(IAudioPlayer audioPlayer, IAudioBitmapCache audioBitmapImageCache, IAudioFileScanner audioFileScanner, IAudioEngine audioEngine, IStorageProviderLocator storageProviderLocator, IClipboardLocator clipboardLocator) { _audioPlayer = audioPlayer; _audioPlayer.PlaylistChanged += OnAudioPlayerPlaylistChanged; _audioPlayer.PlayingSongChanged += OnAudioPlayerPlayingSongChanged; _audioBitmapImageCache = audioBitmapImageCache; _audioFileScanner = audioFileScanner; _audioEngine = audioEngine; _storageProviderLocator = storageProviderLocator; _clipboardLocator = clipboardLocator; } private void OnAudioPlayerPlaylistChanged(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) { switch (e.Action) { case PlaylistUpdateAction.Add: Dispatcher.UIThread.Invoke(() => AddSongs(e.Songs, e.Index)); break; case PlaylistUpdateAction.Remove: Dispatcher.UIThread.Invoke(() => 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 OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e) { //OnPropertyChanged(nameof(PlayingSong)); 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; Dispatcher.UIThread.Invoke(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; IStorageProvider? storageProvider = _storageProviderLocator.Get(); if (storageProvider == null) return; FilePickerOpenOptions openOptions = new() { FileTypeFilter = [GetAudioFileTypes()], AllowMultiple = true }; IReadOnlyList result = await storageProvider.OpenFilePickerAsync(openOptions); string[] fileNames = [.. result.Select(file => file.TryGetLocalPath() ?? string.Empty)]; Song[] songs = await _audioFileScanner.GetSongsAsync(fileNames, cancellationToken); Playlist.AddSongs(songs); } private FilePickerFileType GetAudioFileTypes() { return new("Audo Files") { Patterns = [.. _audioEngine.SupportedFormats] }; } private async Task AddFolderAsync(CancellationToken cancellationToken) { if (Playlist == null) return; IStorageProvider? storageProvider = _storageProviderLocator.Get(); if (storageProvider == null) return; FolderPickerOpenOptions options = new() { AllowMultiple = true }; IReadOnlyList folders = await storageProvider.OpenFolderPickerAsync(options); if (folders.Count == 0) return; string? path = folders[0].TryGetLocalPath(); 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 async Task CutSongsAsync() { if (SelectedPlaylistSongs.Count == 0) return; await CopySelectedSongsToClipboardAsync(); } private async Task CopySongsAsync() { if (SelectedPlaylistSongs.Count == 0) return; await CopySelectedSongsToClipboardAsync(); } private async Task CopySelectedSongsToClipboardAsync() { IClipboard? clipboard = _clipboardLocator.Get(); if (clipboard == null) return; Song[] songs = [.. SelectedPlaylistSongs.Select(playlistSong => playlistSong.Song)]; await clipboard.SetTextAsync(JsonSerializer.Serialize(songs)); } //private async Task CopySelectedSongsToClipboard2Async() //{ // IClipboard? clipboard = Clipboard.Get(); // if (clipboard == null) // return; // Song[] songs = [.. SelectedPlaylistSongs.Select(playlistSong => playlistSong.Song)]; // DataObject dataObject = new(); // dataObject.Set(DataFormats.Text, JsonSerializer.Serialize(songs)); // await clipboard.SetDataObjectAsync(dataObject); //} private bool CanPasteSongs() { return GetSongsFromClipboardAsync().Result.Length > 0; } 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 async Task GetSongsFromClipboardAsync() { IClipboard? clipboard = _clipboardLocator.Get(); if (clipboard == null) return []; string? clipboardText = await clipboard.GetTextAsync(); if (string.IsNullOrWhiteSpace(clipboardText)) return []; try { return JsonSerializer.Deserialize(clipboardText) ?? []; } catch (JsonException) { return []; } } 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 }