From 9245e9c4cc98c29844124d4de989f1d50f6f50f7 Mon Sep 17 00:00:00 2001 From: Brian Bicknell Date: Sat, 22 Mar 2025 19:05:13 -0400 Subject: [PATCH] Added view models and storage providers. --- Harmonia.WinUI/Storage/FilePickerFileType.cs | 9 + Harmonia.WinUI/Storage/FilePickerOptions.cs | 9 + Harmonia.WinUI/Storage/FolderPickerOptions.cs | 6 + Harmonia.WinUI/Storage/IStorageProvider.cs | 10 + .../Storage/StoragePickerViewMode.cs | 7 + .../Storage/WindowsStorageProvider.cs | 90 ++++ Harmonia.WinUI/ViewModels/PlayerViewModel.cs | 303 ++++++++++++ .../ViewModels/PlaylistViewModel.cs | 449 ++++++++++++++++++ 8 files changed, 883 insertions(+) create mode 100644 Harmonia.WinUI/Storage/FilePickerFileType.cs create mode 100644 Harmonia.WinUI/Storage/FilePickerOptions.cs create mode 100644 Harmonia.WinUI/Storage/FolderPickerOptions.cs create mode 100644 Harmonia.WinUI/Storage/IStorageProvider.cs create mode 100644 Harmonia.WinUI/Storage/StoragePickerViewMode.cs create mode 100644 Harmonia.WinUI/Storage/WindowsStorageProvider.cs create mode 100644 Harmonia.WinUI/ViewModels/PlayerViewModel.cs create mode 100644 Harmonia.WinUI/ViewModels/PlaylistViewModel.cs diff --git a/Harmonia.WinUI/Storage/FilePickerFileType.cs b/Harmonia.WinUI/Storage/FilePickerFileType.cs new file mode 100644 index 0000000..a4f2b9c --- /dev/null +++ b/Harmonia.WinUI/Storage/FilePickerFileType.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Harmonia.WinUI.Storage; + +public sealed class FilePickerFileType +{ + public string Name { get; set; } = string.Empty; + public IReadOnlyList Patterns { get; set; } = []; +} \ No newline at end of file diff --git a/Harmonia.WinUI/Storage/FilePickerOptions.cs b/Harmonia.WinUI/Storage/FilePickerOptions.cs new file mode 100644 index 0000000..6df8be5 --- /dev/null +++ b/Harmonia.WinUI/Storage/FilePickerOptions.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Harmonia.WinUI.Storage; + +public class FilePickerOptions +{ + public IReadOnlyList FileTypeFilter { get; set; } = []; + public StoragePickerViewMode ViewMode { get; set; } +} \ No newline at end of file diff --git a/Harmonia.WinUI/Storage/FolderPickerOptions.cs b/Harmonia.WinUI/Storage/FolderPickerOptions.cs new file mode 100644 index 0000000..bac7b8a --- /dev/null +++ b/Harmonia.WinUI/Storage/FolderPickerOptions.cs @@ -0,0 +1,6 @@ +namespace Harmonia.WinUI.Storage; + +public class FolderPickerOptions +{ + public StoragePickerViewMode ViewMode { get; set; } +} \ No newline at end of file diff --git a/Harmonia.WinUI/Storage/IStorageProvider.cs b/Harmonia.WinUI/Storage/IStorageProvider.cs new file mode 100644 index 0000000..cff325e --- /dev/null +++ b/Harmonia.WinUI/Storage/IStorageProvider.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Harmonia.WinUI.Storage; + +public interface IStorageProvider +{ + public Task GetFileAsync(FilePickerOptions? options = null); + public Task GetFilesAsync(FilePickerOptions? options = null); + public Task GetPathAsync(); +} \ No newline at end of file diff --git a/Harmonia.WinUI/Storage/StoragePickerViewMode.cs b/Harmonia.WinUI/Storage/StoragePickerViewMode.cs new file mode 100644 index 0000000..42defd0 --- /dev/null +++ b/Harmonia.WinUI/Storage/StoragePickerViewMode.cs @@ -0,0 +1,7 @@ +namespace Harmonia.WinUI.Storage; + +public enum StoragePickerViewMode +{ + List, + Thumbnail +} \ No newline at end of file diff --git a/Harmonia.WinUI/Storage/WindowsStorageProvider.cs b/Harmonia.WinUI/Storage/WindowsStorageProvider.cs new file mode 100644 index 0000000..494f96f --- /dev/null +++ b/Harmonia.WinUI/Storage/WindowsStorageProvider.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Windows.Storage; +using Windows.Storage.Pickers; + +namespace Harmonia.WinUI.Storage; + +public class WindowsStorageProvider(MainWindow mainWindow) : IStorageProvider +{ + public async Task GetFileAsync(FilePickerOptions? options = null) + { + FileOpenPicker fileOpenPicker = GetFileOpenPicker(options); + InitializePicker(fileOpenPicker); + + StorageFile storageFile = await fileOpenPicker.PickSingleFileAsync(); + + return storageFile.Path; + } + + public async Task GetFilesAsync(FilePickerOptions? options = null) + { + FileOpenPicker fileOpenPicker = GetFileOpenPicker(options); + InitializePicker(fileOpenPicker); + + var storageFiles = await fileOpenPicker.PickMultipleFilesAsync(); + + return [.. storageFiles.Select(storageFile => storageFile.Path)]; + } + + private static FileOpenPicker GetFileOpenPicker(FilePickerOptions? options = null) + { + FilePickerOptions filePickerOptions = options ?? new(); + + FileOpenPicker fileOpenPicker = new() + { + ViewMode = GetViewMode(filePickerOptions.ViewMode) + }; + + string[] filters = GetFilters(filePickerOptions.FileTypeFilter); + + foreach (string filter in filters) + { + fileOpenPicker.FileTypeFilter.Add(filter); + } + + if (filePickerOptions.FileTypeFilter.Count == 0) + { + fileOpenPicker.FileTypeFilter.Add("*"); + } + + return fileOpenPicker; + } + + private static PickerViewMode GetViewMode(StoragePickerViewMode viewMode) + { + return viewMode switch + { + StoragePickerViewMode.Thumbnail => PickerViewMode.Thumbnail, + _ => PickerViewMode.List, + }; + } + + private static string[] GetFilters(IReadOnlyList fileTypes) + { + return [.. fileTypes.SelectMany(fileType => fileType.Patterns)]; + } + + public async Task GetPathAsync() + { + var folderPicker = new FolderPicker + { + ViewMode = PickerViewMode.Thumbnail + }; + folderPicker.FileTypeFilter.Add("*"); + + InitializePicker(folderPicker); + + StorageFolder folder = await folderPicker.PickSingleFolderAsync(); + + return folder.Path; + } + + private void InitializePicker(object target) + { + var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(mainWindow); + WinRT.Interop.InitializeWithWindow.Initialize(target, hWnd); + } +} \ No newline at end of file diff --git a/Harmonia.WinUI/ViewModels/PlayerViewModel.cs b/Harmonia.WinUI/ViewModels/PlayerViewModel.cs new file mode 100644 index 0000000..47f3e4c --- /dev/null +++ b/Harmonia.WinUI/ViewModels/PlayerViewModel.cs @@ -0,0 +1,303 @@ +using CommunityToolkit.Mvvm.Input; +using Harmonia.Core.Models; +using Harmonia.Core.Player; +using Harmonia.WinUI.Caching; +using Harmonia.WinUI.Models; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media.Imaging; +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace Harmonia.WinUI.ViewModels; + +public partial class PlayerViewModel : ViewModelBase +{ + private readonly IAudioPlayer _audioPlayer; + private readonly IAudioBitmapImageCache _audioBitmapImageCache; + private readonly DispatcherTimer _timer; + + private CancellationTokenSource? _songImageCancellationTokenSource; + + private Song? _song; + public Song? Song + { + get + { + return _song; + } + private set + { + SetProperty(ref _song, value); + + CurrentPosition = 0; // What if player is being loaded and returning to save state? + Position = 0; + MaxPosition = value?.Length.TotalSeconds ?? 0; + } + } + + private BitmapImage? _songImageSource; + public BitmapImage? SongImageSource + { + get + { + return _songImageSource; + } + private set + { + SetProperty(ref _songImageSource, value); + } + } + + private double _currentPosition; + public double CurrentPosition + { + get + { + return _currentPosition; + } + set + { + SetProperty(ref _currentPosition, value); + + if (_isPositionChangeInProgress) + _audioPlayer.Position = value; + } + } + + private double _position; + public double Position + { + get + { + return _position; + } + private set + { + SetProperty(ref _position, value); + } + } + + private double _maxPosition; + public double MaxPosition + { + get + { + return _maxPosition; + } + set + { + SetProperty(ref _maxPosition, value); + } + } + + private bool _isPositionChangeInProgress; + public bool IsPositionChangeInProgress + { + get + { + return _isPositionChangeInProgress; + } + set + { + SetProperty(ref _isPositionChangeInProgress, value); + } + } + + public double Volume + { + get + { + return _audioPlayer.Volume; + } + set + { + if (IsMuted) + IsMuted = false; + + _audioPlayer.Volume = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(VolumeState)); + } + } + + public bool IsMuted + { + get + { + return _audioPlayer.IsMuted; + } + set + { + _audioPlayer.IsMuted = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(VolumeState)); + } + } + + public VolumeState VolumeState + { + get + { + if (IsMuted) + return VolumeState.Muted; + + if (Volume == 0) + return VolumeState.Off; + + if (Volume < .33) + return VolumeState.Low; + + if (Volume < .66) + return VolumeState.Medium; + + return VolumeState.High; + } + } + + public bool IsRandom + { + get + { + return _audioPlayer.IsRandom; + } + private set + { + _audioPlayer.IsRandom = value; + OnPropertyChanged(); + } + } + + public RepeatState RepeatState + { + get + { + return _audioPlayer.RepeatState; + } + private set + { + _audioPlayer.RepeatState = value; + OnPropertyChanged(); + } + } + + public ICommand PlaySongCommand => new RelayCommand(Play); + public ICommand PauseSongCommand => new RelayCommand(Pause); + public ICommand StopSongCommand => new RelayCommand(Stop); + public ICommand PreviousSongCommand => new RelayCommand(Previous); + public ICommand NextSongCommand => new RelayCommand(Next); + public ICommand ToggleMuteCommand => new RelayCommand(ToggleMute); + public ICommand ToggleRandomizerCommand => new RelayCommand(ToggleRandomizer); + public ICommand ToggleRepeatCommand => new RelayCommand(ToggleRepeat); + + public PlayerViewModel(IAudioPlayer audioPlayer, IAudioBitmapImageCache audioBitmapCache) + { + _audioPlayer = audioPlayer; + _audioPlayer.PlayingSongChanged += OnPlayingSongChanged; + + _audioBitmapImageCache = audioBitmapCache; + + _timer = new() + { + Interval = TimeSpan.FromMilliseconds(100) + }; + + _timer.Tick += TickTock; + } + + #region Event Handlers + + private void OnPlayingSongChanged(object? sender, EventArgs e) + { + Song = _audioPlayer.PlayingSong?.Song; + Task.Run(UpdateImage); + } + + private async Task UpdateImage() + { + // TODO: Show default picture + if (Song == null) + return; + + if (_songImageCancellationTokenSource != null) + await _songImageCancellationTokenSource.CancelAsync(); + + _songImageCancellationTokenSource = new(); + CancellationToken cancellationToken = _songImageCancellationTokenSource.Token; + + BitmapImage? bitmapImage = await _audioBitmapImageCache.GetAsync(Song, cancellationToken); + + DispatcherQueue.GetForCurrentThread().TryEnqueue(() => SongImageSource = bitmapImage); + } + + private void TickTock(object? sender, object e) + { + Position = _audioPlayer.Position; + + if (IsPositionChangeInProgress) + return; + + CurrentPosition = _audioPlayer.Position; + } + + #endregion + + #region Commands + + public void Play() + { + _audioPlayer.Play(); + } + + public void Pause() + { + _audioPlayer.Pause(); + } + + public void Stop() + { + _audioPlayer.Stop(); + CurrentPosition = 0; + Position = 0; + } + + public void Previous() + { + _audioPlayer.PreviousAsync(); + } + + public void Next() + { + _audioPlayer.NextAsync(); + } + + public void ToggleMute() + { + IsMuted = !IsMuted; + } + + public void ToggleRandomizer() + { + IsRandom = !IsRandom; + } + + public void ToggleRepeat() + { + RepeatState = GetNextRepeatState(); + } + + private RepeatState GetNextRepeatState() + { + return _audioPlayer.RepeatState switch + { + RepeatState.Off => RepeatState.RepeatAll, + RepeatState.RepeatAll => RepeatState.RepeatOne, + RepeatState.RepeatOne => RepeatState.Off, + _ => _audioPlayer.RepeatState, + }; + } + + #endregion +} \ No newline at end of file diff --git a/Harmonia.WinUI/ViewModels/PlaylistViewModel.cs b/Harmonia.WinUI/ViewModels/PlaylistViewModel.cs new file mode 100644 index 0000000..00e4e5f --- /dev/null +++ b/Harmonia.WinUI/ViewModels/PlaylistViewModel.cs @@ -0,0 +1,449 @@ +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.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 Windows.ApplicationModel.DataTransfer; +using Windows.Storage; +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 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) + { + 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) + { + //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; + + 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 +} \ No newline at end of file