Added view models and storage providers.

This commit is contained in:
2025-03-22 19:05:13 -04:00
parent c3a87dee6a
commit 9245e9c4cc
8 changed files with 883 additions and 0 deletions

View 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; } = [];
}

View 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; }
}

View File

@@ -0,0 +1,6 @@
namespace Harmonia.WinUI.Storage;
public class FolderPickerOptions
{
public StoragePickerViewMode ViewMode { get; set; }
}

View 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();
}

View File

@@ -0,0 +1,7 @@
namespace Harmonia.WinUI.Storage;
public enum StoragePickerViewMode
{
List,
Thumbnail
}

View 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);
}
}

View 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
}

View 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
}