diff --git a/Harmonia.Core/Engine/BassAudioEngine.cs b/Harmonia.Core/Engine/BassAudioEngine.cs index e8f5889..7e161b6 100644 --- a/Harmonia.Core/Engine/BassAudioEngine.cs +++ b/Harmonia.Core/Engine/BassAudioEngine.cs @@ -95,11 +95,11 @@ public class BassAudioEngine : IAudioEngine, IDisposable _mediaPlayer.PropertyChanged += OnMediaPlayerPropertyChanged; List supportedFormats = [.. Bass.SupportedFormats.Split(';')]; - supportedFormats.Add(".aac"); - supportedFormats.Add(".m4a"); - supportedFormats.Add(".flac"); - supportedFormats.Add(".opus"); - supportedFormats.Add(".wma"); + //supportedFormats.Add(".aac"); + //supportedFormats.Add(".m4a"); + supportedFormats.Add("*.flac"); + //supportedFormats.Add(".opus"); + //supportedFormats.Add(".wma"); SupportedFormats = [.. supportedFormats]; diff --git a/Harmonia.Core/Harmonia.Core.csproj b/Harmonia.Core/Harmonia.Core.csproj index af2c18a..28fc718 100644 --- a/Harmonia.Core/Harmonia.Core.csproj +++ b/Harmonia.Core/Harmonia.Core.csproj @@ -9,10 +9,10 @@ - - - - + + + + diff --git a/Harmonia.Core/Playlists/PlaylistSong.cs b/Harmonia.Core/Playlists/PlaylistSong.cs index 488c23b..2f61d06 100644 --- a/Harmonia.Core/Playlists/PlaylistSong.cs +++ b/Harmonia.Core/Playlists/PlaylistSong.cs @@ -4,6 +4,6 @@ namespace Harmonia.Core.Playlists; public class PlaylistSong(Song song) { - public string UID { get; } = Guid.NewGuid().ToString(); + public string UID { get; init; } = Guid.NewGuid().ToString(); public Song Song { get; init; } = song; } \ No newline at end of file diff --git a/Harmonia.UI/App.axaml b/Harmonia.UI/App.axaml index fec9fb5..2fddaf3 100644 --- a/Harmonia.UI/App.axaml +++ b/Harmonia.UI/App.axaml @@ -11,6 +11,32 @@ + + @@ -62,6 +88,20 @@ M11 4v1.466a.25.25 0 0 0 .41.192l2.36-1.966a.25.25 0 0 0 0-.384l-2.36-1.966a.25.25 0 0 0-.41.192V3H5a5 5 0 0 0-4.48 7.223.5.5 0 0 0 .896-.446A4 4 0 0 1 5 4zm4.48 1.777a.5.5 0 0 0-.896.446A4 4 0 0 1 11 12H5.001v-1.466a.25.25 0 0 0-.41-.192l-2.36 1.966a.25.25 0 0 0 0 .384l2.36 1.966a.25.25 0 0 0 .41-.192V13h6a5 5 0 0 0 4.48-7.223Z M9 5.5a.5.5 0 0 0-.854-.354l-1.75 1.75a.5.5 0 1 0 .708.708L8 6.707V10.5a.5.5 0 0 0 1 0z + + M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z + M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z + + + M3.5 3.5c-.614-.884-.074-1.962.858-2.5L8 7.226 11.642 1c.932.538 1.472 1.616.858 2.5L8.81 8.61l1.556 2.661a2.5 2.5 0 1 1-.794.637L8 9.73l-1.572 2.177a2.5 2.5 0 1 1-.794-.637L7.19 8.61zm2.5 10a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0m7 0a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0 + + + M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1z + + + 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 + diff --git a/Harmonia.UI/App.axaml.cs b/Harmonia.UI/App.axaml.cs index e701f53..d6d5b6c 100644 --- a/Harmonia.UI/App.axaml.cs +++ b/Harmonia.UI/App.axaml.cs @@ -19,8 +19,9 @@ public partial class App : Application { ServiceCollection services = new(); - services.AddSingleton(); services.AddSingleton(); + + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/Harmonia.UI/Harmonia.UI.csproj b/Harmonia.UI/Harmonia.UI.csproj index fc1f7f0..77ba18d 100644 --- a/Harmonia.UI/Harmonia.UI.csproj +++ b/Harmonia.UI/Harmonia.UI.csproj @@ -18,7 +18,7 @@ - + diff --git a/Harmonia.UI/ViewModels/PlaylistViewModel.cs b/Harmonia.UI/ViewModels/PlaylistViewModel.cs index 93dcddb..cabace3 100644 --- a/Harmonia.UI/ViewModels/PlaylistViewModel.cs +++ b/Harmonia.UI/ViewModels/PlaylistViewModel.cs @@ -7,6 +7,24 @@ using System.Threading; using Avalonia.Media.Imaging; using System.Collections.Concurrent; using Harmonia.UI.Caching; +using Harmonia.Core.Scanner; +using Harmonia.Core.Models; +using Avalonia.Threading; +using System.Windows.Input; +using CommunityToolkit.Mvvm.Input; +using System.Collections.Generic; +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; namespace Harmonia.UI.ViewModels; @@ -14,8 +32,11 @@ public class PlaylistViewModel : ViewModelBase { private readonly IAudioPlayer _audioPlayer; private readonly IAudioBitmapCache _audioBitmapImageCache; + private readonly IAudioFileScanner _audioFileScanner; + private readonly IAudioEngine _audioEngine; private readonly ConcurrentDictionary _bitmapDictionary = []; + public Playlist? Playlist { get; private set; } public PlaylistSong? PlayingSong => _audioPlayer.PlayingSong; private ObservableCollection _playlistSongs = []; @@ -32,34 +53,371 @@ public class PlaylistViewModel : ViewModelBase } } - public PlaylistViewModel(IAudioPlayer audioPlayer, IAudioBitmapCache audioBitmapImageCache) + private ObservableCollection _selectedPlaylistSongs = []; + public ObservableCollection SelectedPlaylistSongs + { + get + { + return _selectedPlaylistSongs; + } + set + { + _selectedPlaylistSongs = value; + OnPropertyChanged(); + } + } + + 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 AsyncRelayCommand(CutSongsAsync, AreSongsSelected); + public ICommand CopySongsCommand => new AsyncRelayCommand(CopySongsAsync, AreSongsSelected); + public ICommand PasteSongsCommand => new AsyncRelayCommand(PasteSongsAsync, CanPasteSongs); + public ICommand OpenFileLocationCommand => new RelayCommand(OpenFileLocation, AreSongsSelected); + + public PlaylistViewModel(IAudioPlayer audioPlayer, IAudioBitmapCache audioBitmapImageCache, IAudioFileScanner audioFileScanner, IAudioEngine audioEngine) { _audioPlayer = audioPlayer; _audioPlayer.PlaylistChanged += OnAudioPlayerPlaylistChanged; _audioPlayer.PlayingSongChanged += OnAudioPlayerPlayingSongChanged; _audioBitmapImageCache = audioBitmapImageCache; + _audioFileScanner = audioFileScanner; + _audioEngine = audioEngine; } private void OnAudioPlayerPlaylistChanged(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]; } + private void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e) + { + switch (e.Action) + { + case PlaylistUpdateAction.Add: + Dispatcher.UIThread.Invoke(() => AddSongs(e.Songs, e.Index)); + break; + case PlaylistUpdateAction.Remove: + Dispatcher.UIThread.Invoke(() => 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); + } + } + + private void RemoveSongsFromCollection(PlaylistSong[] playlistSongs) + { + foreach (PlaylistSong playlistSong in playlistSongs) + { + PlaylistSongs.Remove(playlistSong); + } + } + private void OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e) { OnPropertyChanged(nameof(PlayingSong)); } - public void PlaySong(PlaylistSong playlistSong) + public async Task PlaySongAsync(PlaylistSong playlistSong) { - _audioPlayer.LoadAsync(playlistSong, PlaybackMode.LoadAndPlay); + await _audioPlayer.LoadAsync(playlistSong, PlaybackMode.LoadAndPlay); } public async Task GetBitmapAsync(PlaylistSong playlistSong, CancellationToken cancellationToken) { return await _audioBitmapImageCache.GetAsync(playlistSong.Song, cancellationToken); } + + #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; + + IStorageProvider? storageProvider = StorageProvider.Get(); + + if (storageProvider == null) + return; + + FilePickerOpenOptions openOptions = new() + { + FileTypeFilter = [GetAudioFileTypes()], + AllowMultiple = true + }; + + IReadOnlyList result = await storageProvider.OpenFilePickerAsync(openOptions); + string[] fileNames = [.. result.Select(file => file.TryGetLocalPath() ?? string.Empty)]; + + Song[] songs = await _audioFileScanner.GetSongsAsync(fileNames, cancellationToken); + Playlist.AddSongs(songs); + } + + private FilePickerFileType GetAudioFileTypes() + { + return new("Audo Files") + { + Patterns = [.. _audioEngine.SupportedFormats] + }; + } + + private async Task AddFolderAsync(CancellationToken cancellationToken) + { + if (Playlist == null) + return; + + IStorageProvider? storageProvider = StorageProvider.Get(); + + if (storageProvider == null) + return; + + FolderPickerOpenOptions options = new() + { + AllowMultiple = true + }; + + IReadOnlyList folders = await storageProvider.OpenFolderPickerAsync(options); + + if (folders.Count == 0) + return; + + string? path = folders[0].TryGetLocalPath(); + + 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 async Task CutSongsAsync() + { + if (SelectedPlaylistSongs.Count == 0) + return; + + await CopySelectedSongsToClipboardAsync(); + } + + private async Task CopySongsAsync() + { + if (SelectedPlaylistSongs.Count == 0) + return; + + await CopySelectedSongsToClipboardAsync(); + } + + private async Task CopySelectedSongsToClipboardAsync() + { + IClipboard? clipboard = Clipboard.Get(); + + if (clipboard == null) + return; + + Song[] songs = [.. SelectedPlaylistSongs.Select(playlistSong => playlistSong.Song)]; + + await clipboard.SetTextAsync(JsonSerializer.Serialize(songs)); + } + + //private async Task CopySelectedSongsToClipboard2Async() + //{ + // IClipboard? clipboard = Clipboard.Get(); + + // if (clipboard == null) + // return; + + // Song[] songs = [.. SelectedPlaylistSongs.Select(playlistSong => playlistSong.Song)]; + + // DataObject dataObject = new(); + // dataObject.Set(DataFormats.Text, JsonSerializer.Serialize(songs)); + + // await clipboard.SetDataObjectAsync(dataObject); + //} + + private bool CanPasteSongs() + { + if (Playlist == null) + return false; + + IClipboard? clipboard = Clipboard.Get(); + + if (clipboard == null) + return false; + + string? clipboardText = clipboard.GetTextAsync().Result; + + if (string.IsNullOrWhiteSpace(clipboardText)) + return false; + + Song[] songs = []; + + try + { + songs = JsonSerializer.Deserialize(clipboardText) ?? []; + } + catch (JsonException) + { + return false; + } + + return songs.Length > 0; + } + + private async Task PasteSongsAsync() + { + if (Playlist == null) + return; + + IClipboard? clipboard = Clipboard.Get(); + + if (clipboard == null) + return; + + string? clipboardText = await clipboard.GetTextAsync(); + + if (string.IsNullOrWhiteSpace(clipboardText)) + return; + + Song[] songs = []; + + try + { + songs = JsonSerializer.Deserialize(clipboardText) ?? []; + } + catch (JsonException) + { + + } + + Playlist.AddSongs(songs); + } + + private void OpenFileLocation() + { + if (SelectedPlaylistSongs.Count == 0) + return; + + string argument = "/select, \"" + SelectedPlaylistSongs[0].Song.FileName + "\""; + Process.Start("explorer.exe", argument); + } + + #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 0e9c52c..5216392 100644 --- a/Harmonia.UI/Views/PlaylistView.axaml +++ b/Harmonia.UI/Views/PlaylistView.axaml @@ -6,6 +6,7 @@ xmlns:converter="clr-namespace:Harmonia.UI.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" DataContext="{x:Static vm:ViewModelLocator.PlaylistViewModel}" + Loaded="OnLoaded" x:Class="Harmonia.UI.Views.PlaylistView" x:DataType="vm:PlaylistViewModel"> @@ -46,7 +47,12 @@ - +