423 lines
12 KiB
C#
423 lines
12 KiB
C#
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<string, Bitmap> _bitmapDictionary = [];
|
|
|
|
public Playlist? Playlist { get; private set; }
|
|
public PlaylistSong? PlayingSong => _audioPlayer.PlayingSong;
|
|
|
|
private ObservableCollection<PlaylistSong> _playlistSongs = [];
|
|
public ObservableCollection<PlaylistSong> PlaylistSongs
|
|
{
|
|
get
|
|
{
|
|
return _playlistSongs;
|
|
}
|
|
set
|
|
{
|
|
_playlistSongs = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
private ObservableCollection<PlaylistSong> _selectedPlaylistSongs = [];
|
|
public ObservableCollection<PlaylistSong> 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<Bitmap?> 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<IStorageFile> 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<IStorageFolder> 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<Song[]>(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<Song[]>(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;
|
|
}
|
|
} |