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,15 +1,13 @@
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.Input;
using Harmonia.Core.Caching;
using Harmonia.Core.Imaging;
using Harmonia.Core.Models;
using Harmonia.Core.Player;
using Harmonia.Core.Playlists;
using Harmonia.Core.Scanner;
using Harmonia.UI.Caching;
using Harmonia.UI.Models;
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -20,7 +18,7 @@ namespace Harmonia.UI.ViewModels;
public partial class PlaybackBarViewModel : ViewModelBase, IDisposable
{
private readonly IAudioPlayer _audioPlayer;
private readonly IAudioImageCache _audioImageCache;
private readonly IAudioBitmapCache _audioBitmapCache;
private readonly DispatcherTimer _timer;
private CancellationTokenSource? _audioImageCancellationTokenSource;
@@ -53,7 +51,6 @@ public partial class PlaybackBarViewModel : ViewModelBase, IDisposable
}
private set
{
_songImageSource?.Dispose();
_songImageSource = value;
OnPropertyChanged();
}
@@ -205,14 +202,29 @@ public partial class PlaybackBarViewModel : ViewModelBase, IDisposable
public ICommand ToggleRandomizerCommand => new RelayCommand(ToggleRandomizer);
public ICommand ToggleRepeatCommand => new RelayCommand(ToggleRepeat);
public PlaybackBarViewModel(IAudioPlayer audioPlayer, IAudioImageCache audioImageCache, IPlaylistRepository playlistRepository, IAudioFileScanner audioFileScanner)
public PlaybackBarViewModel(IAudioPlayer audioPlayer, IAudioBitmapCache audioBitmapCache, IPlaylistRepository playlistRepository, IAudioFileScanner audioFileScanner)
{
_audioPlayer = audioPlayer;
_audioPlayer.PlayingSongChanged += OnAudioPlayerPlayingSongChanged;
_audioImageCache = audioImageCache;
_audioBitmapCache = audioBitmapCache;
_timer = new(TimeSpan.FromMilliseconds(100), DispatcherPriority.Default, TickTock);
Task.Run(() => PlayDemoSong(playlistRepository));
}
private async Task PlayDemoSong(IPlaylistRepository playlistRepository)
{
if (playlistRepository.Get().Count == 0)
{
playlistRepository.AddPlaylist();
}
Playlist playlist = playlistRepository.Get().First();
if (playlist.Songs.Count > 0)
await _audioPlayer.LoadAsync(playlist.Songs[0], PlaybackMode.LoadOnly);
}
private void OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e)
@@ -233,21 +245,14 @@ public partial class PlaybackBarViewModel : ViewModelBase, IDisposable
_audioImageCancellationTokenSource = new();
CancellationToken cancellationToken = _audioImageCancellationTokenSource.Token;
SongPictureInfo? songPictureInfo = await _audioImageCache.GetAsync(Song, cancellationToken);
Bitmap? bitmap = await _audioBitmapCache.GetAsync(Song, cancellationToken);
if (songPictureInfo == null)
return;
await Dispatcher.UIThread.InvokeAsync(() => SetSongImageSource(songPictureInfo));
await Dispatcher.UIThread.InvokeAsync(() => SetSongImageSource(bitmap));
}
private void SetSongImageSource(SongPictureInfo songPictureInfo)
private void SetSongImageSource(Bitmap? bitmap)
{
if (songPictureInfo.Data.Length == 0)
return;
using MemoryStream stream = new(songPictureInfo.Data);
SongImageSource = new(stream);
SongImageSource = bitmap;
}
private void TickTock(object? sender, object e)

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