diff --git a/Harmonia.Core/Imaging/AudioImageExtractor.cs b/Harmonia.Core/Imaging/AudioImageExtractor.cs index 2a9b90e..594a7d6 100644 --- a/Harmonia.Core/Imaging/AudioImageExtractor.cs +++ b/Harmonia.Core/Imaging/AudioImageExtractor.cs @@ -68,7 +68,7 @@ public class AudioImageExtractor : IAudioImageExtractor if (string.IsNullOrWhiteSpace(imagePath)) return null; - return await SongPictureInfo.FromFileAsync(path, cancellationToken); + return await SongPictureInfo.FromFileAsync(imagePath, cancellationToken); } } diff --git a/Harmonia.Core/Playlists/Playlist.cs b/Harmonia.Core/Playlists/Playlist.cs index 10a0e1f..2277d98 100644 --- a/Harmonia.Core/Playlists/Playlist.cs +++ b/Harmonia.Core/Playlists/Playlist.cs @@ -31,6 +31,9 @@ public class Playlist if (IsLocked) return; + if (playlistSongs.Length == 0) + return; + int insertIndex = index ?? Songs.Count; Songs.InsertRange(insertIndex, playlistSongs); @@ -158,4 +161,54 @@ public class Playlist PlaylistUpdated?.Invoke(this, eventArgs); } + + public void ImportTags(Song[] songs) + { + foreach (Song song in songs) + { + PlaylistSong[] playlistSongs = [.. Songs.Where(playlistSong => + string.Equals(playlistSong.Song.FileName, song.FileName, StringComparison.OrdinalIgnoreCase))]; + + foreach (PlaylistSong playlistSong in playlistSongs) + { + //playlistSong.Song = song; + } + } + } + + public void RemoveMissingSongs() + { + PlaylistSong[] missingSongs = [.. Songs.Where(playlistSong => + File.Exists(playlistSong.Song.FileName) == false)]; + + if (missingSongs.Length == 0) + return; + + RemoveSongs(missingSongs); + } + + public void RemoveDuplicateSongs() + { + List songsToRemove = []; + + string[] fileNames = Songs + .GroupBy(x => x.Song.FileName) + .Where(x => x.Count() > 1) + .Select(x => x.Key) + .ToArray(); + + foreach (string fileName in fileNames) + { + List songs = Songs.Where(x => + string.Equals(x.Song.FileName, fileName, StringComparison.OrdinalIgnoreCase)).ToList(); + + for (int i = songs.Count - 1; i > 0; i--) + songsToRemove.Add(songs[i]); + } + + if (songsToRemove.Count == 0) + return; + + RemoveSongs(songsToRemove.ToArray()); + } } \ No newline at end of file diff --git a/Harmonia.Core/Playlists/PlaylistRepository.cs b/Harmonia.Core/Playlists/PlaylistRepository.cs index 623ac36..a0094d3 100644 --- a/Harmonia.Core/Playlists/PlaylistRepository.cs +++ b/Harmonia.Core/Playlists/PlaylistRepository.cs @@ -14,6 +14,9 @@ public class PlaylistRepository : JsonFileRepository, IPlaylistReposit { playlist.PlaylistUpdated += OnPlaylistUpdated; } + + if (playlists.Count == 0) + AddPlaylist(); } private void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e) @@ -22,7 +25,6 @@ public class PlaylistRepository : JsonFileRepository, IPlaylistReposit return; Save(playlist); - //PlaylistUpdated?.Invoke(sender, e); } public Playlist? GetPlaylist(PlaylistSong playlistSong) @@ -45,7 +47,6 @@ public class PlaylistRepository : JsonFileRepository, IPlaylistReposit } public event EventHandler? PlaylistAdded; - //public event EventHandler? PlaylistUpdated; public event EventHandler? PlaylistRemoved; public void AddPlaylist() diff --git a/Harmonia.UI/Animations/Animations.cs b/Harmonia.UI/Animations/Animations.cs new file mode 100644 index 0000000..63fb405 --- /dev/null +++ b/Harmonia.UI/Animations/Animations.cs @@ -0,0 +1,58 @@ +using Avalonia; +using Avalonia.Animation; +using Avalonia.Media; +using Avalonia.Styling; +using System; + +namespace Harmonia.UI.Animations +{ + public static class Animations + { + public static Animation SlideIn(Point start, Point end, long duration = 200) + { + return new() + { + Duration = TimeSpan.FromMilliseconds(duration), + Children = + { + new KeyFrame + { + Cue = new Cue(0f), + Setters = + { + new Setter(Visual.OpacityProperty, 0.0), + new Setter(TranslateTransform.XProperty, start.X), + new Setter(TranslateTransform.YProperty, start.Y) + } + }, + new KeyFrame + { + Cue = new Cue(1f), + Setters = + { + new Setter(Visual.OpacityProperty, 1.0), + new Setter(TranslateTransform.XProperty, end.X), + new Setter(TranslateTransform.YProperty, end.Y) + } + } + } + }; + } + + public static Animation SlideFromRight(double offSet, long duration = 200) + { + Point start = new(-offSet, 0); + Point end = new(0, 0); + + return SlideIn(start, end, duration); + } + + public static Animation SlideToRight(double offSet, long duration = 200) + { + Point start = new(0, 0); + Point end = new(offSet, 0); + + return SlideIn(start, end, duration); + } + } +} \ No newline at end of file diff --git a/Harmonia.UI/App.axaml b/Harmonia.UI/App.axaml index 2fddaf3..c0f28d9 100644 --- a/Harmonia.UI/App.axaml +++ b/Harmonia.UI/App.axaml @@ -12,13 +12,8 @@ + + + + @@ -102,6 +116,20 @@ M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1z M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0z + + M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2z + M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466 + + + M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492M5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0 + M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115z + + + M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2m3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2M5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1 + + + M11 1a2 2 0 0 0-2 2v4a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h5V3a3 3 0 0 1 6 0v4a.5.5 0 0 1-1 0V3a2 2 0 0 0-2-2M3 8a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1z + diff --git a/Harmonia.UI/App.axaml.cs b/Harmonia.UI/App.axaml.cs index d6d5b6c..4d72c8f 100644 --- a/Harmonia.UI/App.axaml.cs +++ b/Harmonia.UI/App.axaml.cs @@ -4,6 +4,7 @@ using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; using Harmonia.Core.Extensions; using Harmonia.UI.Caching; +using Harmonia.UI.Platform; using Harmonia.UI.ViewModels; using Harmonia.UI.Views; using Microsoft.Extensions.DependencyInjection; @@ -27,6 +28,8 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddHarmonia(); @@ -65,4 +68,4 @@ public partial class App : Application base.OnFrameworkInitializationCompleted(); } -} +} \ No newline at end of file diff --git a/Harmonia.UI/Controls/AnimatedFlyout.cs b/Harmonia.UI/Controls/AnimatedFlyout.cs new file mode 100644 index 0000000..d76e439 --- /dev/null +++ b/Harmonia.UI/Controls/AnimatedFlyout.cs @@ -0,0 +1,103 @@ +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Media; +using Avalonia.Rendering; +using Avalonia.Styling; +using Avalonia.VisualTree; +using System; +using System.Threading.Tasks; + +namespace Harmonia.UI.Controls; + +public class AnimatedMenuFlyout : MenuFlyout +{ + protected override Control CreatePresenter() + { + Control presenter = base.CreatePresenter(); + presenter.AttachedToVisualTree += OnPresenterAttachedToVisualTree; + + return presenter; + } + + private async void OnPresenterAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + if (sender is not MenuFlyoutPresenter menuFlyoutPresenter) + return; + + await ApplyAnimationAsync(menuFlyoutPresenter); + } + + private async Task ApplyAnimationAsync(MenuFlyoutPresenter presenter) + { + double translateYStart = ShouldSlideDown(presenter) ? - 100 : 100; + + Animation animation = new() + { + Duration = TimeSpan.FromMilliseconds(200), + Children = + { + new KeyFrame + { + Cue = new Cue(0f), + Setters = + { + new Setter(Visual.OpacityProperty, 0.0), + new Setter(TranslateTransform.YProperty, translateYStart) + } + }, + new KeyFrame + { + Cue = new Cue(1f), + Setters = + { + new Setter(Visual.OpacityProperty, 1.0), + new Setter(TranslateTransform.YProperty, 0.0) + } + } + } + }; + + await animation.RunAsync(presenter); + } + + private static bool ShouldSlideDown(MenuFlyoutPresenter presenter) + { + if (presenter.Parent is not Control targetControl) + return true; + + Rect? topLevelBounds = GetTopLevelBounds(); + + if (topLevelBounds == null) + return true; + + Rect targetBounds = targetControl.Bounds; + + double availableSpaceBelow = topLevelBounds.Value.Height - (targetBounds.Y + targetBounds.Height); + double availableSpaceAbove = targetBounds.Y; + + return availableSpaceBelow >= availableSpaceAbove; // Slide down if more space below + } + + private static Rect? GetTopLevelBounds() + { + //Desktop + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + return desktop.MainWindow?.Bounds; + } + //Android (and iOS?) + else if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) + { + IRenderRoot? visualRoot = singleViewPlatform.MainView?.GetVisualRoot(); + + if (visualRoot is TopLevel topLevel) + { + return topLevel.Bounds; + } + } + + return null; + } +} \ No newline at end of file diff --git a/Harmonia.UI/Platform/ClipboardLocator.cs b/Harmonia.UI/Platform/ClipboardLocator.cs new file mode 100644 index 0000000..31091d7 --- /dev/null +++ b/Harmonia.UI/Platform/ClipboardLocator.cs @@ -0,0 +1,17 @@ +using Avalonia.Controls; +using Avalonia.Input.Platform; + +namespace Harmonia.UI.Platform; + +public class ClipboardLocator : PlatformServiceLocator, IClipboardLocator +{ + protected override IClipboard? GetFromWindow(Window mainWindow) + { + return mainWindow.Clipboard; + } + + protected override IClipboard? GetFromTopLevel(TopLevel topLevel) + { + return topLevel.Clipboard; + } +} \ No newline at end of file diff --git a/Harmonia.UI/Platform/IClipboardLocator.cs b/Harmonia.UI/Platform/IClipboardLocator.cs new file mode 100644 index 0000000..c2cf7e1 --- /dev/null +++ b/Harmonia.UI/Platform/IClipboardLocator.cs @@ -0,0 +1,8 @@ +using Avalonia.Input.Platform; + +namespace Harmonia.UI.Platform; + +public interface IClipboardLocator : IPlatformServiceLocator +{ + +} \ No newline at end of file diff --git a/Harmonia.UI/Platform/IPlatformServiceLocator.cs b/Harmonia.UI/Platform/IPlatformServiceLocator.cs new file mode 100644 index 0000000..f7d1128 --- /dev/null +++ b/Harmonia.UI/Platform/IPlatformServiceLocator.cs @@ -0,0 +1,6 @@ +namespace Harmonia.UI.Platform; + +public interface IPlatformServiceLocator +{ + T? Get(); +} \ No newline at end of file diff --git a/Harmonia.UI/Platform/IStorageProviderLocator.cs b/Harmonia.UI/Platform/IStorageProviderLocator.cs new file mode 100644 index 0000000..8b132ee --- /dev/null +++ b/Harmonia.UI/Platform/IStorageProviderLocator.cs @@ -0,0 +1,8 @@ +using Avalonia.Platform.Storage; + +namespace Harmonia.UI.Platform; + +public interface IStorageProviderLocator : IPlatformServiceLocator +{ + +} \ No newline at end of file diff --git a/Harmonia.UI/Platform/PlatformServiceLocator.cs b/Harmonia.UI/Platform/PlatformServiceLocator.cs new file mode 100644 index 0000000..c46c954 --- /dev/null +++ b/Harmonia.UI/Platform/PlatformServiceLocator.cs @@ -0,0 +1,40 @@ +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls; +using Avalonia.Rendering; +using Avalonia; +using Avalonia.VisualTree; + +namespace Harmonia.UI.Platform; + +public abstract class PlatformServiceLocator : IPlatformServiceLocator +{ + public T? Get() + { + //Desktop + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + if (desktop.MainWindow == null) + return default; + + return GetFromWindow(desktop.MainWindow); + } + //Android (and iOS?) + else if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) + { + if (singleViewPlatform.MainView == null) + return default; + + IRenderRoot? visualRoot = singleViewPlatform.MainView.GetVisualRoot(); + + if (visualRoot is TopLevel topLevel) + { + return GetFromTopLevel(topLevel); + } + } + + return default; + } + + protected abstract T? GetFromWindow(Window mainWindow); + protected abstract T? GetFromTopLevel(TopLevel topLevel); +} \ No newline at end of file diff --git a/Harmonia.UI/Platform/StorageProviderLocator.cs b/Harmonia.UI/Platform/StorageProviderLocator.cs new file mode 100644 index 0000000..24effba --- /dev/null +++ b/Harmonia.UI/Platform/StorageProviderLocator.cs @@ -0,0 +1,17 @@ +using Avalonia.Controls; +using Avalonia.Platform.Storage; + +namespace Harmonia.UI.Platform; + +public class StorageProviderLocator : PlatformServiceLocator, IStorageProviderLocator +{ + protected override IStorageProvider? GetFromWindow(Window mainWindow) + { + return mainWindow.StorageProvider; + } + + protected override IStorageProvider? GetFromTopLevel(TopLevel topLevel) + { + return topLevel.StorageProvider; + } +} \ No newline at end of file diff --git a/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs b/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs index f359e9c..e072c54 100644 --- a/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs +++ b/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs @@ -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) diff --git a/Harmonia.UI/ViewModels/PlaylistViewModel.cs b/Harmonia.UI/ViewModels/PlaylistViewModel.cs index cabace3..8dd3345 100644 --- a/Harmonia.UI/ViewModels/PlaylistViewModel.cs +++ b/Harmonia.UI/ViewModels/PlaylistViewModel.cs @@ -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 _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 _filteredPlaylistSongs = []; + public ObservableCollection FilteredPlaylistSongs + { + get + { + return _filteredPlaylistSongs; + } + set + { + _filteredPlaylistSongs = value; + OnPropertyChanged(); + } + } + private ObservableCollection _selectedPlaylistSongs = []; public ObservableCollection 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 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; - } } \ No newline at end of file diff --git a/Harmonia.UI/Views/PlaylistView.axaml b/Harmonia.UI/Views/PlaylistView.axaml index 5216392..82f4072 100644 --- a/Harmonia.UI/Views/PlaylistView.axaml +++ b/Harmonia.UI/Views/PlaylistView.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="clr-namespace:Harmonia.UI.ViewModels" xmlns:converter="clr-namespace:Harmonia.UI.Converters" + xmlns:controls="clr-namespace:Harmonia.UI.Controls" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" DataContext="{x:Static vm:ViewModelLocator.PlaylistViewModel}" Loaded="OnLoaded" @@ -46,11 +47,109 @@ - + + + + + + + + + + + + + + + +