using Harmonia.Core.Player; using Harmonia.Core.Playlists; using System; using System.Collections.ObjectModel; using System.Threading.Tasks; using System.Threading; using Avalonia.Media.Imaging; using System.Collections.Concurrent; using Harmonia.UI.Caching; using Harmonia.Core.Scanner; using Harmonia.Core.Models; using Avalonia.Threading; using System.Windows.Input; using CommunityToolkit.Mvvm.Input; using System.Collections.Generic; using System.Linq; using System.Text.Json; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls; using Avalonia.Input.Platform; using Avalonia.VisualTree; using Avalonia.Rendering; using System.Diagnostics; using Avalonia.Platform.Storage; using Harmonia.Core.Engine; using Avalonia.Input; 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 ConcurrentDictionary _bitmapDictionary = []; public Playlist? Playlist { get; private set; } public PlaylistSong? PlayingSong => _audioPlayer.PlayingSong; private ObservableCollection _playlistSongs = []; public ObservableCollection PlaylistSongs { get { return _playlistSongs; } set { _playlistSongs = value; OnPropertyChanged(); } } 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 PlaylistViewModel(IAudioPlayer audioPlayer, IAudioBitmapCache audioBitmapImageCache, IAudioFileScanner audioFileScanner, IAudioEngine audioEngine) { _audioPlayer = audioPlayer; _audioPlayer.PlaylistChanged += OnAudioPlayerPlaylistChanged; _audioPlayer.PlayingSongChanged += OnAudioPlayerPlayingSongChanged; _audioBitmapImageCache = audioBitmapImageCache; _audioFileScanner = audioFileScanner; _audioEngine = audioEngine; } 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]; } 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); } } private void RemoveSongsFromCollection(PlaylistSong[] playlistSongs) { foreach (PlaylistSong playlistSong in playlistSongs) { PlaylistSongs.Remove(playlistSong); } } private void OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e) { OnPropertyChanged(nameof(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 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 = StorageProvider.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 = StorageProvider.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 = Clipboard.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() { if (Playlist == null) return false; IClipboard? clipboard = Clipboard.Get(); if (clipboard == null) return false; string? clipboardText = clipboard.GetTextAsync().Result; if (string.IsNullOrWhiteSpace(clipboardText)) return false; Song[] songs = []; try { songs = JsonSerializer.Deserialize(clipboardText) ?? []; } catch (JsonException) { return false; } return songs.Length > 0; } private async Task PasteSongsAsync() { if (Playlist == null) return; IClipboard? clipboard = Clipboard.Get(); if (clipboard == null) return; string? clipboardText = await clipboard.GetTextAsync(); if (string.IsNullOrWhiteSpace(clipboardText)) return; Song[] songs = []; try { songs = JsonSerializer.Deserialize(clipboardText) ?? []; } catch (JsonException) { } Playlist.AddSongs(songs); } private void OpenFileLocation() { if (SelectedPlaylistSongs.Count == 0) return; string argument = "/select, \"" + SelectedPlaylistSongs[0].Song.FileName + "\""; Process.Start("explorer.exe", argument); } #endregion } public class Clipboard { public static IClipboard? Get() { //Desktop if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { return desktop.MainWindow?.Clipboard; } //Android (and iOS?) else if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) { IRenderRoot? visualRoot = singleViewPlatform.MainView?.GetVisualRoot(); if (visualRoot is TopLevel topLevel) { return topLevel.Clipboard; } } return null; } } public class StorageProvider { public static IStorageProvider? Get() { //Desktop if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { return desktop.MainWindow?.StorageProvider; } //Android (and iOS?) else if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) { IRenderRoot? visualRoot = singleViewPlatform.MainView?.GetVisualRoot(); if (visualRoot is TopLevel topLevel) { return topLevel.StorageProvider; } } return null; } }