Added ContextMenu/Flyout animations. Added platform services. Use bitmap cache for all views.

This commit is contained in:
2025-03-18 09:31:32 -04:00
parent 7c70eb3814
commit 9214e97100
17 changed files with 649 additions and 169 deletions

View File

@@ -1,30 +1,28 @@
using Harmonia.Core.Player;
using Harmonia.Core.Playlists;
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Threading;
using Avalonia.Input.Platform;
using Avalonia.Media.Imaging;
using System.Collections.Concurrent;
using Harmonia.UI.Caching;
using Harmonia.Core.Scanner;
using Harmonia.Core.Models;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using System.Windows.Input;
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.UI.Caching;
using Harmonia.UI.Platform;
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 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;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using System.Windows.Input;
using Timer = System.Timers.Timer;
namespace Harmonia.UI.ViewModels;
@@ -34,8 +32,12 @@ public class PlaylistViewModel : ViewModelBase
private readonly IAudioBitmapCache _audioBitmapImageCache;
private readonly IAudioFileScanner _audioFileScanner;
private readonly IAudioEngine _audioEngine;
private readonly IStorageProviderLocator _storageProviderLocator;
private readonly IClipboardLocator _clipboardLocator;
private readonly ConcurrentDictionary<string, Bitmap> _bitmapDictionary = [];
private Timer? _filterTimer;
public Playlist? Playlist { get; private set; }
public PlaylistSong? PlayingSong => _audioPlayer.PlayingSong;
@@ -53,6 +55,35 @@ public class PlaylistViewModel : ViewModelBase
}
}
private string? _filter;
public string? Filter
{
get
{
return _filter;
}
set
{
_filter = value;
OnPropertyChanged();
RestartFilterTimer();
}
}
private ObservableCollection<PlaylistSong> _filteredPlaylistSongs = [];
public ObservableCollection<PlaylistSong> FilteredPlaylistSongs
{
get
{
return _filteredPlaylistSongs;
}
set
{
_filteredPlaylistSongs = value;
OnPropertyChanged();
}
}
private ObservableCollection<PlaylistSong> _selectedPlaylistSongs = [];
public ObservableCollection<PlaylistSong> SelectedPlaylistSongs
{
@@ -76,7 +107,15 @@ public class PlaylistViewModel : ViewModelBase
public ICommand PasteSongsCommand => new AsyncRelayCommand(PasteSongsAsync, CanPasteSongs);
public ICommand OpenFileLocationCommand => new RelayCommand(OpenFileLocation, AreSongsSelected);
public PlaylistViewModel(IAudioPlayer audioPlayer, IAudioBitmapCache audioBitmapImageCache, IAudioFileScanner audioFileScanner, IAudioEngine audioEngine)
public ICommand RefreshTagsCommand => new RelayCommand(RefreshTags);
public ICommand RemoveMissingSongsCommand => new RelayCommand(RemoveMissingSongs);
public ICommand RemoveDuplicateSongsCommand => new RelayCommand(RemoveDuplicateSongs);
public PlaylistViewModel(IAudioPlayer audioPlayer, IAudioBitmapCache audioBitmapImageCache,
IAudioFileScanner audioFileScanner,
IAudioEngine audioEngine,
IStorageProviderLocator storageProviderLocator,
IClipboardLocator clipboardLocator)
{
_audioPlayer = audioPlayer;
_audioPlayer.PlaylistChanged += OnAudioPlayerPlaylistChanged;
@@ -85,6 +124,8 @@ public class PlaylistViewModel : ViewModelBase
_audioBitmapImageCache = audioBitmapImageCache;
_audioFileScanner = audioFileScanner;
_audioEngine = audioEngine;
_storageProviderLocator = storageProviderLocator;
_clipboardLocator = clipboardLocator;
}
private void OnAudioPlayerPlaylistChanged(object? sender, EventArgs e)
@@ -104,6 +145,7 @@ public class PlaylistViewModel : ViewModelBase
PlaylistSong[] playlistSongs = _audioPlayer.Playlist?.Songs.ToArray() ?? [];
PlaylistSongs = [.. playlistSongs];
UpdateFilteredSongs();
}
private void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e)
@@ -128,6 +170,8 @@ public class PlaylistViewModel : ViewModelBase
{
PlaylistSongs.Insert(currentIndex++, playlistSong);
}
UpdateFilteredSongs();
}
private void RemoveSongsFromCollection(PlaylistSong[] playlistSongs)
@@ -136,6 +180,8 @@ public class PlaylistViewModel : ViewModelBase
{
PlaylistSongs.Remove(playlistSong);
}
UpdateFilteredSongs();
}
private void OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e)
@@ -153,6 +199,70 @@ public class PlaylistViewModel : ViewModelBase
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;
Dispatcher.UIThread.Invoke(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()
@@ -173,7 +283,7 @@ public class PlaylistViewModel : ViewModelBase
if (Playlist == null)
return;
IStorageProvider? storageProvider = StorageProvider.Get();
IStorageProvider? storageProvider = _storageProviderLocator.Get();
if (storageProvider == null)
return;
@@ -204,7 +314,7 @@ public class PlaylistViewModel : ViewModelBase
if (Playlist == null)
return;
IStorageProvider? storageProvider = StorageProvider.Get();
IStorageProvider? storageProvider = _storageProviderLocator.Get();
if (storageProvider == null)
return;
@@ -277,7 +387,7 @@ public class PlaylistViewModel : ViewModelBase
private async Task CopySelectedSongsToClipboardAsync()
{
IClipboard? clipboard = Clipboard.Get();
IClipboard? clipboard = _clipboardLocator.Get();
if (clipboard == null)
return;
@@ -307,7 +417,7 @@ public class PlaylistViewModel : ViewModelBase
if (Playlist == null)
return false;
IClipboard? clipboard = Clipboard.Get();
IClipboard? clipboard = _clipboardLocator.Get();
if (clipboard == null)
return false;
@@ -336,7 +446,7 @@ public class PlaylistViewModel : ViewModelBase
if (Playlist == null)
return;
IClipboard? clipboard = Clipboard.Get();
IClipboard? clipboard = _clipboardLocator.Get();
if (clipboard == null)
return;
@@ -369,55 +479,20 @@ public class PlaylistViewModel : ViewModelBase
Process.Start("explorer.exe", argument);
}
private void RefreshTags()
{
//Playlist?.RefreshTags();
}
private void RemoveMissingSongs()
{
Playlist?.RemoveMissingSongs();
}
private void RemoveDuplicateSongs()
{
Playlist?.RemoveDuplicateSongs();
}
#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;
}
}