Added view models and storage providers.
This commit is contained in:
9
Harmonia.WinUI/Storage/FilePickerFileType.cs
Normal file
9
Harmonia.WinUI/Storage/FilePickerFileType.cs
Normal file
@@ -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<string> Patterns { get; set; } = [];
|
||||||
|
}
|
||||||
9
Harmonia.WinUI/Storage/FilePickerOptions.cs
Normal file
9
Harmonia.WinUI/Storage/FilePickerOptions.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Storage;
|
||||||
|
|
||||||
|
public class FilePickerOptions
|
||||||
|
{
|
||||||
|
public IReadOnlyList<FilePickerFileType> FileTypeFilter { get; set; } = [];
|
||||||
|
public StoragePickerViewMode ViewMode { get; set; }
|
||||||
|
}
|
||||||
6
Harmonia.WinUI/Storage/FolderPickerOptions.cs
Normal file
6
Harmonia.WinUI/Storage/FolderPickerOptions.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Harmonia.WinUI.Storage;
|
||||||
|
|
||||||
|
public class FolderPickerOptions
|
||||||
|
{
|
||||||
|
public StoragePickerViewMode ViewMode { get; set; }
|
||||||
|
}
|
||||||
10
Harmonia.WinUI/Storage/IStorageProvider.cs
Normal file
10
Harmonia.WinUI/Storage/IStorageProvider.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Storage;
|
||||||
|
|
||||||
|
public interface IStorageProvider
|
||||||
|
{
|
||||||
|
public Task<string> GetFileAsync(FilePickerOptions? options = null);
|
||||||
|
public Task<string[]> GetFilesAsync(FilePickerOptions? options = null);
|
||||||
|
public Task<string> GetPathAsync();
|
||||||
|
}
|
||||||
7
Harmonia.WinUI/Storage/StoragePickerViewMode.cs
Normal file
7
Harmonia.WinUI/Storage/StoragePickerViewMode.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Harmonia.WinUI.Storage;
|
||||||
|
|
||||||
|
public enum StoragePickerViewMode
|
||||||
|
{
|
||||||
|
List,
|
||||||
|
Thumbnail
|
||||||
|
}
|
||||||
90
Harmonia.WinUI/Storage/WindowsStorageProvider.cs
Normal file
90
Harmonia.WinUI/Storage/WindowsStorageProvider.cs
Normal file
@@ -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<string> GetFileAsync(FilePickerOptions? options = null)
|
||||||
|
{
|
||||||
|
FileOpenPicker fileOpenPicker = GetFileOpenPicker(options);
|
||||||
|
InitializePicker(fileOpenPicker);
|
||||||
|
|
||||||
|
StorageFile storageFile = await fileOpenPicker.PickSingleFileAsync();
|
||||||
|
|
||||||
|
return storageFile.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string[]> 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<FilePickerFileType> fileTypes)
|
||||||
|
{
|
||||||
|
return [.. fileTypes.SelectMany(fileType => fileType.Patterns)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
303
Harmonia.WinUI/ViewModels/PlayerViewModel.cs
Normal file
303
Harmonia.WinUI/ViewModels/PlayerViewModel.cs
Normal file
@@ -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
|
||||||
|
}
|
||||||
449
Harmonia.WinUI/ViewModels/PlaylistViewModel.cs
Normal file
449
Harmonia.WinUI/ViewModels/PlaylistViewModel.cs
Normal file
@@ -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<PlaylistSong> _playlistSongs = [];
|
||||||
|
public ObservableCollection<PlaylistSong> 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<PlaylistSong> _filteredPlaylistSongs = [];
|
||||||
|
public ObservableCollection<PlaylistSong> FilteredPlaylistSongs
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _filteredPlaylistSongs;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _filteredPlaylistSongs, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObservableCollection<PlaylistSong> _selectedPlaylistSongs = [];
|
||||||
|
public ObservableCollection<PlaylistSong> 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<BitmapImage?> 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<PlaylistSong> 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<Song[]> GetSongsFromClipboardAsync()
|
||||||
|
{
|
||||||
|
DataPackageView dataPackageView = Clipboard.GetContent();
|
||||||
|
string data = await dataPackageView.GetTextAsync(StandardDataFormats.Text);
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<Song[]>(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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user